Commit 89414f173e6c34411c7fbe984932aef1f3670380
1 parent
34781d42
feat(Form): 新增 ApiRadioGroup 组件
Showing
6 changed files
with
182 additions
and
4 deletions
mock/demo/select-demo.ts
1 | import { MockMethod } from 'vite-plugin-mock'; | 1 | import { MockMethod } from 'vite-plugin-mock'; |
2 | import { resultSuccess } from '../_util'; | 2 | import { resultSuccess } from '../_util'; |
3 | 3 | ||
4 | -const demoList = (keyword) => { | 4 | +const demoList = (keyword, count = 20) => { |
5 | const result = { | 5 | const result = { |
6 | list: [] as any[], | 6 | list: [] as any[], |
7 | }; | 7 | }; |
8 | - for (let index = 0; index < 20; index++) { | 8 | + for (let index = 0; index < count; index++) { |
9 | result.list.push({ | 9 | result.list.push({ |
10 | name: `${keyword ?? ''}选项${index}`, | 10 | name: `${keyword ?? ''}选项${index}`, |
11 | id: `${index}`, | 11 | id: `${index}`, |
@@ -20,9 +20,9 @@ export default [ | @@ -20,9 +20,9 @@ export default [ | ||
20 | timeout: 1000, | 20 | timeout: 1000, |
21 | method: 'get', | 21 | method: 'get', |
22 | response: ({ query }) => { | 22 | response: ({ query }) => { |
23 | - const { keyword } = query; | 23 | + const { keyword, count } = query; |
24 | console.log(keyword); | 24 | console.log(keyword); |
25 | - return resultSuccess(demoList(keyword)); | 25 | + return resultSuccess(demoList(keyword, count)); |
26 | }, | 26 | }, |
27 | }, | 27 | }, |
28 | ] as MockMethod[]; | 28 | ] as MockMethod[]; |
src/components/Form/index.ts
@@ -9,5 +9,6 @@ export { useForm } from './src/hooks/useForm'; | @@ -9,5 +9,6 @@ export { useForm } from './src/hooks/useForm'; | ||
9 | export { default as ApiSelect } from './src/components/ApiSelect.vue'; | 9 | export { default as ApiSelect } from './src/components/ApiSelect.vue'; |
10 | export { default as RadioButtonGroup } from './src/components/RadioButtonGroup.vue'; | 10 | export { default as RadioButtonGroup } from './src/components/RadioButtonGroup.vue'; |
11 | export { default as ApiTreeSelect } from './src/components/ApiTreeSelect.vue'; | 11 | export { default as ApiTreeSelect } from './src/components/ApiTreeSelect.vue'; |
12 | +export { default as ApiRadioGroup } from './src/components/ApiRadioGroup.vue'; | ||
12 | 13 | ||
13 | export { BasicForm }; | 14 | export { BasicForm }; |
src/components/Form/src/componentMap.ts
@@ -21,6 +21,7 @@ import { | @@ -21,6 +21,7 @@ import { | ||
21 | Divider, | 21 | Divider, |
22 | } from 'ant-design-vue'; | 22 | } from 'ant-design-vue'; |
23 | 23 | ||
24 | +import ApiRadioGroup from './components/ApiRadioGroup.vue'; | ||
24 | import RadioButtonGroup from './components/RadioButtonGroup.vue'; | 25 | import RadioButtonGroup from './components/RadioButtonGroup.vue'; |
25 | import ApiSelect from './components/ApiSelect.vue'; | 26 | import ApiSelect from './components/ApiSelect.vue'; |
26 | import ApiTreeSelect from './components/ApiTreeSelect.vue'; | 27 | import ApiTreeSelect from './components/ApiTreeSelect.vue'; |
@@ -43,6 +44,7 @@ componentMap.set('Select', Select); | @@ -43,6 +44,7 @@ componentMap.set('Select', Select); | ||
43 | componentMap.set('ApiSelect', ApiSelect); | 44 | componentMap.set('ApiSelect', ApiSelect); |
44 | componentMap.set('TreeSelect', TreeSelect); | 45 | componentMap.set('TreeSelect', TreeSelect); |
45 | componentMap.set('ApiTreeSelect', ApiTreeSelect); | 46 | componentMap.set('ApiTreeSelect', ApiTreeSelect); |
47 | +componentMap.set('ApiRadioGroup', ApiRadioGroup); | ||
46 | componentMap.set('Switch', Switch); | 48 | componentMap.set('Switch', Switch); |
47 | componentMap.set('RadioButtonGroup', RadioButtonGroup); | 49 | componentMap.set('RadioButtonGroup', RadioButtonGroup); |
48 | componentMap.set('RadioGroup', Radio.Group); | 50 | componentMap.set('RadioGroup', Radio.Group); |
src/components/Form/src/components/ApiRadioGroup.vue
0 → 100644
1 | +<!-- | ||
2 | + * @Description:It is troublesome to implement radio button group in the form. So it is extracted independently as a separate component | ||
3 | +--> | ||
4 | +<template> | ||
5 | + <RadioGroup v-bind="attrs" v-model:value="state" button-style="solid" @change="handleChange"> | ||
6 | + <template v-for="item in getOptions" :key="`${item.value}`"> | ||
7 | + <RadioButton v-if="props.isBtn" :value="item.value" :disabled="item.disabled"> | ||
8 | + {{ item.label }} | ||
9 | + </RadioButton> | ||
10 | + <Radio v-else :value="item.value" :disabled="item.disabled"> | ||
11 | + {{ item.label }} | ||
12 | + </Radio> | ||
13 | + </template> | ||
14 | + </RadioGroup> | ||
15 | +</template> | ||
16 | +<script lang="ts"> | ||
17 | + import { defineComponent, PropType, ref, watchEffect, computed, unref, watch } from 'vue'; | ||
18 | + import { Radio } from 'ant-design-vue'; | ||
19 | + import { isFunction } from '/@/utils/is'; | ||
20 | + import { useRuleFormItem } from '/@/hooks/component/useFormItem'; | ||
21 | + import { useAttrs } from '/@/hooks/core/useAttrs'; | ||
22 | + import { propTypes } from '/@/utils/propTypes'; | ||
23 | + import { get, omit } from 'lodash-es'; | ||
24 | + import { useI18n } from '/@/hooks/web/useI18n'; | ||
25 | + type OptionsItem = { label: string; value: string | number | boolean; disabled?: boolean }; | ||
26 | + | ||
27 | + export default defineComponent({ | ||
28 | + name: 'ApiRadioGroup', | ||
29 | + components: { | ||
30 | + RadioGroup: Radio.Group, | ||
31 | + RadioButton: Radio.Button, | ||
32 | + Radio, | ||
33 | + }, | ||
34 | + props: { | ||
35 | + api: { | ||
36 | + type: Function as PropType<(arg?: Recordable | string) => Promise<OptionsItem[]>>, | ||
37 | + default: null, | ||
38 | + }, | ||
39 | + params: { | ||
40 | + type: [Object, String] as PropType<Recordable | string>, | ||
41 | + default: () => ({}), | ||
42 | + }, | ||
43 | + value: { | ||
44 | + type: [String, Number, Boolean] as PropType<string | number | boolean>, | ||
45 | + }, | ||
46 | + isBtn: { | ||
47 | + type: [Boolean] as PropType<boolean>, | ||
48 | + default: false, | ||
49 | + }, | ||
50 | + numberToString: propTypes.bool, | ||
51 | + resultField: propTypes.string.def(''), | ||
52 | + labelField: propTypes.string.def('label'), | ||
53 | + valueField: propTypes.string.def('value'), | ||
54 | + immediate: propTypes.bool.def(true), | ||
55 | + }, | ||
56 | + emits: ['options-change', 'change'], | ||
57 | + setup(props, { emit }) { | ||
58 | + const options = ref<OptionsItem[]>([]); | ||
59 | + const loading = ref(false); | ||
60 | + const isFirstLoad = ref(true); | ||
61 | + const emitData = ref<any[]>([]); | ||
62 | + const attrs = useAttrs(); | ||
63 | + const { t } = useI18n(); | ||
64 | + // Embedded in the form, just use the hook binding to perform form verification | ||
65 | + const [state] = useRuleFormItem(props); | ||
66 | + | ||
67 | + // Processing options value | ||
68 | + const getOptions = computed(() => { | ||
69 | + const { labelField, valueField, numberToString } = props; | ||
70 | + | ||
71 | + return unref(options).reduce((prev, next: Recordable) => { | ||
72 | + if (next) { | ||
73 | + const value = next[valueField]; | ||
74 | + prev.push({ | ||
75 | + label: next[labelField], | ||
76 | + value: numberToString ? `${value}` : value, | ||
77 | + ...omit(next, [labelField, valueField]), | ||
78 | + }); | ||
79 | + } | ||
80 | + return prev; | ||
81 | + }, [] as OptionsItem[]); | ||
82 | + }); | ||
83 | + | ||
84 | + watchEffect(() => { | ||
85 | + props.immediate && fetch(); | ||
86 | + }); | ||
87 | + | ||
88 | + watch( | ||
89 | + () => props.params, | ||
90 | + () => { | ||
91 | + !unref(isFirstLoad) && fetch(); | ||
92 | + }, | ||
93 | + { deep: true }, | ||
94 | + ); | ||
95 | + | ||
96 | + async function fetch() { | ||
97 | + const api = props.api; | ||
98 | + if (!api || !isFunction(api)) return; | ||
99 | + options.value = []; | ||
100 | + try { | ||
101 | + loading.value = true; | ||
102 | + const res = await api(props.params); | ||
103 | + if (Array.isArray(res)) { | ||
104 | + options.value = res; | ||
105 | + emitChange(); | ||
106 | + return; | ||
107 | + } | ||
108 | + if (props.resultField) { | ||
109 | + options.value = get(res, props.resultField) || []; | ||
110 | + } | ||
111 | + emitChange(); | ||
112 | + } catch (error) { | ||
113 | + console.warn(error); | ||
114 | + } finally { | ||
115 | + loading.value = false; | ||
116 | + } | ||
117 | + } | ||
118 | + | ||
119 | + function emitChange() { | ||
120 | + emit('options-change', unref(getOptions)); | ||
121 | + } | ||
122 | + | ||
123 | + function handleChange(_, ...args) { | ||
124 | + emitData.value = args; | ||
125 | + } | ||
126 | + | ||
127 | + return { state, getOptions, attrs, loading, t, handleChange, props }; | ||
128 | + }, | ||
129 | + }); | ||
130 | +</script> |
src/components/Form/src/types/index.ts
@@ -92,6 +92,7 @@ export type ComponentType = | @@ -92,6 +92,7 @@ export type ComponentType = | ||
92 | | 'ApiSelect' | 92 | | 'ApiSelect' |
93 | | 'TreeSelect' | 93 | | 'TreeSelect' |
94 | | 'ApiTreeSelect' | 94 | | 'ApiTreeSelect' |
95 | + | 'ApiRadioGroup' | ||
95 | | 'RadioButtonGroup' | 96 | | 'RadioButtonGroup' |
96 | | 'RadioGroup' | 97 | | 'RadioGroup' |
97 | | 'Checkbox' | 98 | | 'Checkbox' |
src/views/demo/form/index.vue
@@ -411,6 +411,50 @@ | @@ -411,6 +411,50 @@ | ||
411 | }, | 411 | }, |
412 | }, | 412 | }, |
413 | { | 413 | { |
414 | + field: 'field34', | ||
415 | + component: 'ApiRadioGroup', | ||
416 | + label: '远程Radio', | ||
417 | + helpMessage: ['ApiRadioGroup组件', '使用接口提供的数据生成选项'], | ||
418 | + required: true, | ||
419 | + componentProps: { | ||
420 | + api: optionsListApi, | ||
421 | + params: { | ||
422 | + count: 2, | ||
423 | + }, | ||
424 | + resultField: 'list', | ||
425 | + // use name as label | ||
426 | + labelField: 'name', | ||
427 | + // use id as value | ||
428 | + valueField: 'id', | ||
429 | + }, | ||
430 | + defaultValue: '1', | ||
431 | + colProps: { | ||
432 | + span: 8, | ||
433 | + }, | ||
434 | + }, | ||
435 | + { | ||
436 | + field: 'field35', | ||
437 | + component: 'ApiRadioGroup', | ||
438 | + label: '远程Radio', | ||
439 | + helpMessage: ['ApiRadioGroup组件', '使用接口提供的数据生成选项'], | ||
440 | + required: true, | ||
441 | + componentProps: { | ||
442 | + api: optionsListApi, | ||
443 | + params: { | ||
444 | + count: 2, | ||
445 | + }, | ||
446 | + resultField: 'list', | ||
447 | + // use name as label | ||
448 | + labelField: 'name', | ||
449 | + // use id as value | ||
450 | + valueField: 'id', | ||
451 | + isBtn: true, | ||
452 | + }, | ||
453 | + colProps: { | ||
454 | + span: 8, | ||
455 | + }, | ||
456 | + }, | ||
457 | + { | ||
414 | field: 'divider-linked', | 458 | field: 'divider-linked', |
415 | component: 'Divider', | 459 | component: 'Divider', |
416 | label: '字段联动', | 460 | label: '字段联动', |