Commit ac1a36950259844822c6300a00710b040dfc2640
1 parent
4ff1c408
perf(form): improve the form function
Showing
23 changed files
with
344 additions
and
100 deletions
CHANGELOG.zh_CN.md
... | ... | @@ -8,6 +8,9 @@ |
8 | 8 | - 新增主框架外页面示例 |
9 | 9 | - `route.meta` 新增`currentActiveMenu`,`hideTab`,`hideMenu`参数 用于控制详情页面包屑级菜单显示隐藏。 |
10 | 10 | - 新增面包屑导航示例 |
11 | +- form: 新增`suffix`属性,用于配置后缀内容 | |
12 | +- form: 新增远程下拉`ApiSelect`及示例 | |
13 | +- form: 新增`autoFocusFirstItem`配置。用于配置是否聚焦表单第一个输入框 | |
11 | 14 | |
12 | 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 | 22 | }, |
23 | 23 | "dependencies": { |
24 | 24 | "@iconify/iconify": "^2.0.0-rc.4", |
25 | - "@vueuse/core": "^4.0.0", | |
25 | + "@vueuse/core": "^4.0.1", | |
26 | 26 | "ant-design-vue": "^2.0.0-rc.5", |
27 | 27 | "apexcharts": "^3.23.0", |
28 | 28 | "axios": "^0.21.1", |
... | ... | @@ -35,7 +35,7 @@ |
35 | 35 | "path-to-regexp": "^6.2.0", |
36 | 36 | "qrcode": "^1.4.4", |
37 | 37 | "sortablejs": "^1.12.0", |
38 | - "vditor": "^3.7.3", | |
38 | + "vditor": "^3.7.4", | |
39 | 39 | "vue": "^3.0.4", |
40 | 40 | "vue-i18n": "9.0.0-beta.14", |
41 | 41 | "vue-router": "^4.0.1", |
... | ... | @@ -48,7 +48,7 @@ |
48 | 48 | "devDependencies": { |
49 | 49 | "@commitlint/cli": "^11.0.0", |
50 | 50 | "@commitlint/config-conventional": "^11.0.0", |
51 | - "@iconify/json": "^1.1.276", | |
51 | + "@iconify/json": "^1.1.277", | |
52 | 52 | "@ls-lint/ls-lint": "^1.9.2", |
53 | 53 | "@purge-icons/generated": "^0.4.1", |
54 | 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 | 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 | 4 | <slot name="formHeader" /> |
5 | 5 | <template v-for="schema in getSchema" :key="schema.field"> |
6 | 6 | <FormItem |
... | ... | @@ -18,7 +18,6 @@ |
18 | 18 | </FormItem> |
19 | 19 | </template> |
20 | 20 | |
21 | - <!-- --> | |
22 | 21 | <FormAction |
23 | 22 | v-bind="{ ...getProps, ...advanceState }" |
24 | 23 | @toggle-advanced="handleToggleAdvanced" |
... | ... | @@ -46,8 +45,10 @@ |
46 | 45 | import useAdvanced from './hooks/useAdvanced'; |
47 | 46 | import { useFormEvents } from './hooks/useFormEvents'; |
48 | 47 | import { createFormContext } from './hooks/useFormContext'; |
48 | + import { useAutoFocus } from './hooks/useAutoFocus'; | |
49 | 49 | |
50 | 50 | import { basicProps } from './props'; |
51 | + import { useDesign } from '/@/hooks/web/useDesign'; | |
51 | 52 | |
52 | 53 | export default defineComponent({ |
53 | 54 | name: 'BasicForm', |
... | ... | @@ -71,6 +72,8 @@ |
71 | 72 | const schemaRef = ref<Nullable<FormSchema[]>>(null); |
72 | 73 | const formElRef = ref<Nullable<FormActionType>>(null); |
73 | 74 | |
75 | + const { prefixCls } = useDesign('basic-form'); | |
76 | + | |
74 | 77 | // Get the basic configuration of the form |
75 | 78 | const getProps = computed( |
76 | 79 | (): FormProps => { |
... | ... | @@ -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 | 93 | // Get uniform row style |
82 | 94 | const getRowWrapStyle = computed( |
83 | 95 | (): CSSProperties => { |
... | ... | @@ -115,7 +127,7 @@ |
115 | 127 | defaultValueRef, |
116 | 128 | }); |
117 | 129 | |
118 | - const { transformDateFunc, fieldMapToTime } = toRefs(props); | |
130 | + const { transformDateFunc, fieldMapToTime, autoFocusFirstItem } = toRefs(props); | |
119 | 131 | |
120 | 132 | const { handleFormValues, initDefault } = useFormValues({ |
121 | 133 | transformDateFuncRef: transformDateFunc, |
... | ... | @@ -125,6 +137,13 @@ |
125 | 137 | formModel, |
126 | 138 | }); |
127 | 139 | |
140 | + useAutoFocus({ | |
141 | + getSchema, | |
142 | + autoFocusFirstItem, | |
143 | + isInitedDefault: isInitedDefaultRef, | |
144 | + formElRef: formElRef as Ref<FormActionType>, | |
145 | + }); | |
146 | + | |
128 | 147 | const { |
129 | 148 | handleSubmit, |
130 | 149 | setFieldsValue, |
... | ... | @@ -217,8 +236,51 @@ |
217 | 236 | getSchema, |
218 | 237 | formActionType, |
219 | 238 | setFormModel, |
239 | + prefixCls, | |
240 | + getFormClass, | |
220 | 241 | ...formActionType, |
221 | 242 | }; |
222 | 243 | }, |
223 | 244 | }); |
224 | 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 | 19 | } from 'ant-design-vue'; |
20 | 20 | |
21 | 21 | import RadioButtonGroup from './components/RadioButtonGroup.vue'; |
22 | +import ApiSelect from './components/ApiSelect.vue'; | |
22 | 23 | import { BasicUpload } from '/@/components/Upload'; |
23 | 24 | |
24 | 25 | const componentMap = new Map<ComponentType, Component>(); |
... | ... | @@ -32,6 +33,7 @@ componentMap.set('InputNumber', InputNumber); |
32 | 33 | componentMap.set('AutoComplete', AutoComplete); |
33 | 34 | |
34 | 35 | componentMap.set('Select', Select); |
36 | +componentMap.set('ApiSelect', ApiSelect); | |
35 | 37 | // componentMap.set('SelectOptGroup', Select.OptGroup); |
36 | 38 | // componentMap.set('SelectOption', Select.Option); |
37 | 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 '../types/form'; |
3 | 3 | import type { FormSchema } from '../types/form'; |
4 | 4 | import type { ValidationRule } from 'ant-design-vue/lib/form/Form'; |
5 | 5 | import type { TableActionType } from '/@/components/Table'; |
6 | -import type { ComponentType } from '../types'; | |
7 | 6 | |
8 | 7 | import { defineComponent, computed, unref, toRefs } from 'vue'; |
9 | 8 | import { Form, Col } from 'ant-design-vue'; |
... | ... | @@ -16,7 +15,6 @@ import { createPlaceholderMessage, setComponentRuleType } from '../helper'; |
16 | 15 | import { upperFirst, cloneDeep } from 'lodash-es'; |
17 | 16 | |
18 | 17 | import { useItemLabelWidth } from '../hooks/useLabelWidth'; |
19 | -import { isNumber } from '/@/utils/is'; | |
20 | 18 | import { useI18n } from '/@/hooks/web/useI18n'; |
21 | 19 | |
22 | 20 | export default defineComponent({ |
... | ... | @@ -81,7 +79,7 @@ export default defineComponent({ |
81 | 79 | if (!isFunction(componentProps)) { |
82 | 80 | return componentProps; |
83 | 81 | } |
84 | - return componentProps({ schema, tableAction, formModel, formActionType }) || {}; | |
82 | + return componentProps({ schema, tableAction, formModel, formActionType }) ?? {}; | |
85 | 83 | }); |
86 | 84 | |
87 | 85 | const getDisable = computed(() => { |
... | ... | @@ -99,7 +97,7 @@ export default defineComponent({ |
99 | 97 | return disabled; |
100 | 98 | }); |
101 | 99 | |
102 | - function getShow() { | |
100 | + const getShow = computed(() => { | |
103 | 101 | const { show, ifShow } = props.schema; |
104 | 102 | const { showAdvancedButton } = props.formProps; |
105 | 103 | const itemIsAdvanced = showAdvancedButton |
... | ... | @@ -124,7 +122,7 @@ export default defineComponent({ |
124 | 122 | } |
125 | 123 | isShow = isShow && itemIsAdvanced; |
126 | 124 | return { isShow, isIfShow }; |
127 | - } | |
125 | + }); | |
128 | 126 | |
129 | 127 | function handleRules(): ValidationRule[] { |
130 | 128 | const { |
... | ... | @@ -171,7 +169,7 @@ export default defineComponent({ |
171 | 169 | } |
172 | 170 | } |
173 | 171 | |
174 | - // 最大输入长度规则校验 | |
172 | + // Maximum input length rule check | |
175 | 173 | const characterInx = rules.findIndex((val) => val.max); |
176 | 174 | if (characterInx !== -1 && !rules[characterInx].validator) { |
177 | 175 | rules[characterInx].message = |
... | ... | @@ -180,20 +178,6 @@ export default defineComponent({ |
180 | 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 | 181 | function renderComponent() { |
198 | 182 | const { |
199 | 183 | renderComponentContent, |
... | ... | @@ -217,7 +201,6 @@ export default defineComponent({ |
217 | 201 | |
218 | 202 | const value = target ? (isCheck ? target.checked : target.value) : e; |
219 | 203 | props.setFormModel(field, value); |
220 | - // props.formModel[field] = value; | |
221 | 204 | }, |
222 | 205 | }; |
223 | 206 | const Comp = componentMap.get(component) as typeof defineComponent; |
... | ... | @@ -233,7 +216,7 @@ export default defineComponent({ |
233 | 216 | |
234 | 217 | const isCreatePlaceholder = !propsData.disabled && autoSetPlaceHolder; |
235 | 218 | let placeholder; |
236 | - // RangePicker place为数组 | |
219 | + // RangePicker place is an array | |
237 | 220 | if (isCreatePlaceholder && component !== 'RangePicker' && component) { |
238 | 221 | placeholder = unref(getComponentsProps)?.placeholder || createPlaceholderMessage(component); |
239 | 222 | } |
... | ... | @@ -242,7 +225,7 @@ export default defineComponent({ |
242 | 225 | propsData.formValues = unref(getValues); |
243 | 226 | |
244 | 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 | 231 | const compAttr: Recordable = { |
... | ... | @@ -284,7 +267,7 @@ export default defineComponent({ |
284 | 267 | } |
285 | 268 | |
286 | 269 | function renderItem() { |
287 | - const { itemProps, slot, render, field } = props.schema; | |
270 | + const { itemProps, slot, render, field, suffix } = props.schema; | |
288 | 271 | const { labelCol, wrapperCol } = unref(itemLabelWidthProp); |
289 | 272 | const { colon } = props.formProps; |
290 | 273 | |
... | ... | @@ -296,17 +279,27 @@ export default defineComponent({ |
296 | 279 | : renderComponent(); |
297 | 280 | }; |
298 | 281 | |
282 | + const showSuffix = !!suffix; | |
283 | + | |
284 | + const getSuffix = isFunction(suffix) ? suffix(unref(getValues)) : suffix; | |
285 | + | |
299 | 286 | return ( |
300 | 287 | <Form.Item |
301 | 288 | name={field} |
302 | 289 | colon={colon} |
290 | + class={{ 'suffix-item': showSuffix }} | |
303 | 291 | {...(itemProps as Recordable)} |
304 | 292 | label={renderLabelHelpMessage()} |
305 | 293 | rules={handleRules()} |
306 | 294 | labelCol={labelCol} |
307 | 295 | wrapperCol={wrapperCol} |
308 | 296 | > |
309 | - {() => getContent()} | |
297 | + {() => ( | |
298 | + <> | |
299 | + {getContent()} | |
300 | + {showSuffix && <span class="suffix">{getSuffix}</span>} | |
301 | + </> | |
302 | + )} | |
310 | 303 | </Form.Item> |
311 | 304 | ); |
312 | 305 | } |
... | ... | @@ -317,7 +310,7 @@ export default defineComponent({ |
317 | 310 | const { baseColProps = {} } = props.formProps; |
318 | 311 | |
319 | 312 | const realColProps = { ...baseColProps, ...colProps }; |
320 | - const { isIfShow, isShow } = getShow(); | |
313 | + const { isIfShow, isShow } = unref(getShow); | |
321 | 314 | |
322 | 315 | const getContent = () => { |
323 | 316 | return colSlot | ... | ... |
src/components/Form/src/helper.ts
1 | 1 | import type { ValidationRule } from 'ant-design-vue/lib/form/Form'; |
2 | 2 | import type { ComponentType } from './types/index'; |
3 | 3 | import { useI18n } from '/@/hooks/web/useI18n'; |
4 | +import { isNumber } from '/@/utils/is'; | |
4 | 5 | |
5 | 6 | const { t } = useI18n(); |
6 | 7 | |
... | ... | @@ -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 | 1 | import type { ColEx } from '../types'; |
2 | 2 | import type { AdvanceState } from '../types/hooks'; |
3 | -import { ComputedRef, Ref } from 'vue'; | |
3 | +import type { ComputedRef, Ref } from 'vue'; | |
4 | 4 | import type { FormProps, FormSchema } from '../types/form'; |
5 | 5 | |
6 | 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 'vue'; |
6 | 6 | |
7 | 7 | import { isArray, isFunction, isObject, isString } from '/@/utils/is'; |
8 | 8 | import { deepMerge, unique } from '/@/utils'; |
9 | -import { dateItemType } from '../helper'; | |
9 | +import { dateItemType, handleInputNumberValue } from '../helper'; | |
10 | 10 | import moment from 'moment'; |
11 | 11 | import { cloneDeep } from 'lodash-es'; |
12 | 12 | import { error } from '/@/utils/log'; |
... | ... | @@ -49,29 +49,32 @@ export function useFormEvents({ |
49 | 49 | /** |
50 | 50 | * @description: Set form value |
51 | 51 | */ |
52 | - async function setFieldsValue(values: any): Promise<void> { | |
52 | + async function setFieldsValue(values: Recordable): Promise<void> { | |
53 | 53 | const fields = unref(getSchema) |
54 | 54 | .map((item) => item.field) |
55 | 55 | .filter(Boolean); |
56 | 56 | |
57 | 57 | const validKeys: string[] = []; |
58 | 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 | 63 | // 0| '' is allow |
61 | - if (element !== undefined && element !== null && fields.includes(key)) { | |
64 | + if (value !== undefined && value !== null && fields.includes(key)) { | |
62 | 65 | // time type |
63 | 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 | 70 | arr.push(moment(ele)); |
68 | 71 | } |
69 | 72 | formModel[key] = arr; |
70 | 73 | } else { |
71 | - formModel[key] = moment(element); | |
74 | + formModel[key] = moment(value); | |
72 | 75 | } |
73 | 76 | } else { |
74 | - formModel[key] = element; | |
77 | + formModel[key] = value; | |
75 | 78 | } |
76 | 79 | validKeys.push(key); |
77 | 80 | } | ... | ... |
src/components/Form/src/props.ts
... | ... | @@ -65,6 +65,8 @@ export const basicProps = { |
65 | 65 | actionColOptions: Object as PropType<Partial<ColEx>>, |
66 | 66 | // 显示重置按钮 |
67 | 67 | showResetButton: propTypes.bool.def(true), |
68 | + // 是否聚焦第一个输入框,只在第一个表单项为input的时候作用 | |
69 | + autoFocusFirstItem: propTypes.bool, | |
68 | 70 | // 重置按钮配置 |
69 | 71 | resetButtonOptions: Object as PropType<Partial<ButtonProps>>, |
70 | 72 | ... | ... |
src/components/Form/src/types/form.ts
... | ... | @@ -82,6 +82,8 @@ export interface FormProps { |
82 | 82 | rulesMessageJoinLabel?: boolean; |
83 | 83 | // Whether to show collapse and expand buttons |
84 | 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 | 87 | // Automatically collapse over the specified number of rows |
86 | 88 | autoAdvancedLine?: number; |
87 | 89 | // Whether to show the operation button |
... | ... | @@ -139,6 +141,8 @@ export interface FormSchema { |
139 | 141 | // Required |
140 | 142 | required?: boolean; |
141 | 143 | |
144 | + suffix?: string | number | ((values: RenderCallbackParams) => string | number); | |
145 | + | |
142 | 146 | // Validation rules |
143 | 147 | rules?: Rule[]; |
144 | 148 | // Check whether the information is added to the label | ... | ... |
src/components/Form/src/types/index.ts
src/design/ant/index.less
... | ... | @@ -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 | 52 | // ==============empty============== |
84 | 53 | // ================================= |
85 | 54 | .ant-empty-image { | ... | ... |
src/locales/lang/en/component/form.ts
src/locales/lang/zh_CN/component/form.ts
src/utils/http/axios/index.ts
... | ... | @@ -105,28 +105,29 @@ const transform: AxiosTransform = { |
105 | 105 | if (apiUrl && isString(apiUrl)) { |
106 | 106 | config.url = `${apiUrl}${config.url}`; |
107 | 107 | } |
108 | + const params = config.params || {}; | |
108 | 109 | if (config.method?.toUpperCase() === RequestEnum.GET) { |
109 | - if (!isString(config.params)) { | |
110 | + if (!isString(params)) { | |
110 | 111 | config.data = { |
111 | 112 | // 给 get 请求加上时间戳参数,避免从缓存中拿数据。 |
112 | - params: Object.assign(config.params || {}, createNow(joinTime, false)), | |
113 | + params: Object.assign(params || {}, createNow(joinTime, false)), | |
113 | 114 | }; |
114 | 115 | } else { |
115 | 116 | // 兼容restful风格 |
116 | - config.url = config.url + config.params + `${createNow(joinTime, true)}`; | |
117 | + config.url = config.url + params + `${createNow(joinTime, true)}`; | |
117 | 118 | config.params = undefined; |
118 | 119 | } |
119 | 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 | 124 | config.params = undefined; |
124 | 125 | if (joinParamsToUrl) { |
125 | 126 | config.url = setObjToUrlParams(config.url as string, config.data); |
126 | 127 | } |
127 | 128 | } else { |
128 | 129 | // 兼容restful风格 |
129 | - config.url = config.url + config.params; | |
130 | + config.url = config.url + params; | |
130 | 131 | config.params = undefined; |
131 | 132 | } |
132 | 133 | } | ... | ... |
src/views/demo/form/RuleForm.vue
src/views/demo/form/index.vue
... | ... | @@ -2,6 +2,7 @@ |
2 | 2 | <div class="m-4"> |
3 | 3 | <CollapseContainer title="基础示例"> |
4 | 4 | <BasicForm |
5 | + autoFocusFirstItem | |
5 | 6 | :labelWidth="100" |
6 | 7 | :schemas="schemas" |
7 | 8 | :actionColOptions="{ span: 24 }" |
... | ... | @@ -16,11 +17,13 @@ |
16 | 17 | import { CollapseContainer } from '/@/components/Container/index'; |
17 | 18 | import { useMessage } from '/@/hooks/web/useMessage'; |
18 | 19 | |
20 | + import { optionsListApi } from '/@/api/demo/select'; | |
19 | 21 | const schemas: FormSchema[] = [ |
20 | 22 | { |
21 | 23 | field: 'field1', |
22 | 24 | component: 'Input', |
23 | 25 | label: '字段1', |
26 | + | |
24 | 27 | colProps: { |
25 | 28 | span: 8, |
26 | 29 | }, |
... | ... | @@ -46,7 +49,7 @@ |
46 | 49 | { |
47 | 50 | field: 'field2', |
48 | 51 | component: 'Input', |
49 | - label: '字段2', | |
52 | + label: '带后缀', | |
50 | 53 | defaultValue: '111', |
51 | 54 | colProps: { |
52 | 55 | span: 8, |
... | ... | @@ -56,6 +59,7 @@ |
56 | 59 | console.log(e); |
57 | 60 | }, |
58 | 61 | }, |
62 | + suffix: '天', | |
59 | 63 | }, |
60 | 64 | { |
61 | 65 | field: 'field3', |
... | ... | @@ -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 | 229 | field: 'field20', |
213 | 230 | component: 'InputNumber', | ... | ... |
yarn.lock
... | ... | @@ -1076,10 +1076,10 @@ |
1076 | 1076 | resolved "https://registry.npmjs.org/@iconify/iconify/-/iconify-2.0.0-rc.4.tgz#46098fb544a4eb3af724219e4955c9022801835e" |
1077 | 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 | 1084 | "@intlify/core-base@9.0.0-beta.14": |
1085 | 1085 | version "9.0.0-beta.14" |
... | ... | @@ -1831,18 +1831,18 @@ |
1831 | 1831 | vscode-languageserver-textdocument "^1.0.1" |
1832 | 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 | 1838 | dependencies: |
1839 | - "@vueuse/shared" "4.0.0" | |
1839 | + "@vueuse/shared" "4.0.1" | |
1840 | 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 | 1846 | dependencies: |
1847 | 1847 | vue-demi latest |
1848 | 1848 | |
... | ... | @@ -8039,10 +8039,10 @@ vary@^1.1.2: |
8039 | 8039 | resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" |
8040 | 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 | 8046 | dependencies: |
8047 | 8047 | diff-match-patch "^1.0.5" |
8048 | 8048 | ... | ... |