feat: improve ApiSelect component (#5075)

* feat: improve `ApiSelect` component

* chore: `ApiSelect` props name changed
This commit is contained in:
Netfan 2024-12-09 12:47:33 +08:00 committed by GitHub
parent 305549e7f2
commit d085736bac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 209 additions and 22 deletions

View File

@ -49,6 +49,7 @@ const withDefaultPlaceholder = <T extends Component>(
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明 // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType = export type ComponentType =
| 'ApiSelect' | 'ApiSelect'
| 'ApiTreeSelect'
| 'AutoComplete' | 'AutoComplete'
| 'Checkbox' | 'Checkbox'
| 'CheckboxGroup' | 'CheckboxGroup'
@ -88,7 +89,23 @@ async function initComponentAdapter() {
component: Select, component: Select,
loadingSlot: 'suffixIcon', loadingSlot: 'suffixIcon',
visibleEvent: 'onDropdownVisibleChange', visibleEvent: 'onDropdownVisibleChange',
modelField: 'value', modelPropName: 'value',
},
slots,
);
},
ApiTreeSelect: (props, { attrs, slots }) => {
return h(
ApiSelect,
{
...props,
...attrs,
component: TreeSelect,
fieldNames: { label: 'label', value: 'value', children: 'children' },
loadingSlot: 'suffixIcon',
modelPropName: 'value',
optionsPropName: 'treeData',
visibleEvent: 'onVisibleChange',
}, },
slots, slots,
); );

View File

@ -48,6 +48,7 @@ const withDefaultPlaceholder = <T extends Component>(
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明 // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType = export type ComponentType =
| 'ApiSelect' | 'ApiSelect'
| 'ApiTreeSelect'
| 'Checkbox' | 'Checkbox'
| 'CheckboxGroup' | 'CheckboxGroup'
| 'DatePicker' | 'DatePicker'
@ -77,7 +78,23 @@ async function initComponentAdapter() {
...attrs, ...attrs,
component: ElSelectV2, component: ElSelectV2,
loadingSlot: 'loading', loadingSlot: 'loading',
visibleEvent: 'onDropdownVisibleChange', visibleEvent: 'onVisibleChange',
},
slots,
);
},
ApiTreeSelect: (props, { attrs, slots }) => {
return h(
ApiSelect,
{
...props,
...attrs,
component: ElTreeSelect,
props: { label: 'label', children: 'children' },
nodeKey: 'value',
loadingSlot: 'loading',
optionsPropName: 'data',
visibleEvent: 'onVisibleChange',
}, },
slots, slots,
); );

View File

@ -6,6 +6,7 @@ import { Page } from '@vben/common-ui';
import { ElButton, ElCard, ElCheckbox, ElMessage } from 'element-plus'; import { ElButton, ElCard, ElCheckbox, ElMessage } from 'element-plus';
import { useVbenForm } from '#/adapter/form'; import { useVbenForm } from '#/adapter/form';
import { getAllMenusApi } from '#/api';
const [Form, formApi] = useVbenForm({ const [Form, formApi] = useVbenForm({
commonConfig: { commonConfig: {
@ -21,6 +22,44 @@ const [Form, formApi] = useVbenForm({
ElMessage.success(`表单数据:${JSON.stringify(values)}`); ElMessage.success(`表单数据:${JSON.stringify(values)}`);
}, },
schema: [ schema: [
{
// #/adapter.ts
component: 'ApiSelect',
//
componentProps: {
// options
afterFetch: (data: { name: string; path: string }[]) => {
return data.map((item: any) => ({
label: item.name,
value: item.path,
}));
},
//
api: getAllMenusApi,
placeholder: '请选择',
},
//
fieldName: 'api',
// label
label: 'ApiSelect',
},
{
component: 'ApiTreeSelect',
//
componentProps: {
//
api: getAllMenusApi,
childrenField: 'children',
// options
labelField: 'name',
placeholder: '请选择',
valueField: 'path',
},
//
fieldName: 'apiTree',
// label
label: 'ApiTreeSelect',
},
{ {
component: 'Input', component: 'Input',
fieldName: 'string', fieldName: 'string',

View File

@ -45,6 +45,7 @@ const withDefaultPlaceholder = <T extends Component>(
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明 // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType = export type ComponentType =
| 'ApiSelect' | 'ApiSelect'
| 'ApiTreeSelect'
| 'Checkbox' | 'Checkbox'
| 'CheckboxGroup' | 'CheckboxGroup'
| 'DatePicker' | 'DatePicker'
@ -74,7 +75,24 @@ async function initComponentAdapter() {
...props, ...props,
...attrs, ...attrs,
component: NSelect, component: NSelect,
modelField: 'value', modelPropName: 'value',
},
slots,
);
},
ApiTreeSelect: (props, { attrs, slots }) => {
return h(
ApiSelect,
{
...props,
...attrs,
component: NTreeSelect,
nodeKey: 'value',
loadingSlot: 'arrow',
keyField: 'value',
modelPropName: 'value',
optionsPropName: 'options',
visibleEvent: 'onVisibleChange',
}, },
slots, slots,
); );

View File

@ -4,6 +4,7 @@ import { Page } from '@vben/common-ui';
import { NButton, NCard, useMessage } from 'naive-ui'; import { NButton, NCard, useMessage } from 'naive-ui';
import { useVbenForm } from '#/adapter/form'; import { useVbenForm } from '#/adapter/form';
import { getAllMenusApi } from '#/api';
const message = useMessage(); const message = useMessage();
const [Form, formApi] = useVbenForm({ const [Form, formApi] = useVbenForm({
@ -20,6 +21,44 @@ const [Form, formApi] = useVbenForm({
message.success(`表单数据:${JSON.stringify(values)}`); message.success(`表单数据:${JSON.stringify(values)}`);
}, },
schema: [ schema: [
{
// #/adapter.ts
component: 'ApiSelect',
//
componentProps: {
// options
afterFetch: (data: { name: string; path: string }[]) => {
return data.map((item: any) => ({
label: item.name,
value: item.path,
}));
},
//
api: getAllMenusApi,
placeholder: '请选择',
},
//
fieldName: 'api',
// label
label: 'ApiSelect',
},
{
component: 'ApiTreeSelect',
//
componentProps: {
//
api: getAllMenusApi,
childrenField: 'children',
// options
labelField: 'name',
placeholder: '请选择',
valueField: 'path',
},
//
fieldName: 'apiTree',
// label
label: 'ApiTreeSelect',
},
{ {
component: 'Input', component: 'Input',
fieldName: 'string', fieldName: 'string',

View File

@ -10,30 +10,47 @@ import { objectOmit } from '@vueuse/core';
type OptionsItem = { type OptionsItem = {
[name: string]: any; [name: string]: any;
children?: OptionsItem[];
disabled?: boolean; disabled?: boolean;
label?: string; label?: string;
value?: string; value?: string;
}; };
interface Props { interface Props {
// /** 组件 */
component: VNode; component: VNode;
/** 是否将value从数字转为string */
numberToString?: boolean; numberToString?: boolean;
/** 获取options数据的函数 */
api?: (arg?: any) => Promise<OptionsItem[] | Record<string, any>>; api?: (arg?: any) => Promise<OptionsItem[] | Record<string, any>>;
/** 传递给api的参数 */
params?: Record<string, any>; params?: Record<string, any>;
/** 从api返回的结果中提取options数组的字段名 */
resultField?: string; resultField?: string;
/** label字段名 */
labelField?: string; labelField?: string;
/** children字段名需要层级数据的组件可用 */
childrenField?: string;
/** value字段名 */
valueField?: string; valueField?: string;
/** 组件接收options数据的属性名 */
optionsPropName?: string;
/** 是否立即调用api */
immediate?: boolean; immediate?: boolean;
/** 每次`visibleEvent`事件发生时都重新请求数据 */
alwaysLoad?: boolean; alwaysLoad?: boolean;
/** 在api请求之前的回调函数 */
beforeFetch?: AnyPromiseFunction<any, any>; beforeFetch?: AnyPromiseFunction<any, any>;
/** 在api请求之后的回调函数 */
afterFetch?: AnyPromiseFunction<any, any>; afterFetch?: AnyPromiseFunction<any, any>;
/** 直接传入选项数据也作为api返回空数据时的后备数据 */
options?: OptionsItem[]; options?: OptionsItem[];
// /** 组件的插槽名称,用来显示一个"加载中"的图标 */
loadingSlot?: string; loadingSlot?: string;
// /** 触发api请求的事件名 */
visibleEvent?: string; visibleEvent?: string;
modelField?: string; /** 组件的v-model属性名默认为modelValue。部分组件可能为value */
modelPropName?: string;
} }
defineOptions({ name: 'ApiSelect', inheritAttrs: false }); defineOptions({ name: 'ApiSelect', inheritAttrs: false });
@ -41,6 +58,8 @@ defineOptions({ name: 'ApiSelect', inheritAttrs: false });
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
labelField: 'label', labelField: 'label',
valueField: 'value', valueField: 'value',
childrenField: '',
optionsPropName: 'options',
resultField: '', resultField: '',
visibleEvent: '', visibleEvent: '',
numberToString: false, numberToString: false,
@ -50,7 +69,7 @@ const props = withDefaults(defineProps<Props>(), {
loadingSlot: '', loadingSlot: '',
beforeFetch: undefined, beforeFetch: undefined,
afterFetch: undefined, afterFetch: undefined,
modelField: 'modelValue', modelPropName: 'modelValue',
api: undefined, api: undefined,
options: () => [], options: () => [],
}); });
@ -69,29 +88,34 @@ const loading = ref(false);
const isFirstLoaded = ref(false); const isFirstLoaded = ref(false);
const getOptions = computed(() => { const getOptions = computed(() => {
const { labelField, valueField, numberToString } = props; const { labelField, valueField, childrenField, numberToString } = props;
const data: OptionsItem[] = [];
const refOptionsData = unref(refOptions); const refOptionsData = unref(refOptions);
for (const next of refOptionsData) { function transformData(data: OptionsItem[]): OptionsItem[] {
if (next) { return data.map((item) => {
const value = get(next, valueField); const value = get(item, valueField);
data.push({ return {
...objectOmit(next, [labelField, valueField]), ...objectOmit(item, [labelField, valueField, childrenField]),
label: get(next, labelField), label: get(item, labelField),
value: numberToString ? `${value}` : value, value: numberToString ? `${value}` : value,
}); ...(childrenField && item[childrenField]
} ? { children: transformData(item[childrenField]) }
: {}),
};
});
} }
const data: OptionsItem[] = transformData(refOptionsData);
return data.length > 0 ? data : props.options; return data.length > 0 ? data : props.options;
}); });
const bindProps = computed(() => { const bindProps = computed(() => {
return { return {
[props.modelField]: unref(modelValue), [props.modelPropName]: unref(modelValue),
[`onUpdate:${props.modelField}`]: (val: string) => { [props.optionsPropName]: unref(getOptions),
[`onUpdate:${props.modelPropName}`]: (val: string) => {
modelValue.value = val; modelValue.value = val;
}, },
...objectOmit(attrs, ['onUpdate:value']), ...objectOmit(attrs, ['onUpdate:value']),
@ -168,7 +192,6 @@ function emitChange() {
<component <component
:is="component" :is="component"
v-bind="bindProps" v-bind="bindProps"
:options="getOptions"
:placeholder="$attrs.placeholder" :placeholder="$attrs.placeholder"
> >
<template v-for="item in Object.keys($slots)" #[item]="data"> <template v-for="item in Object.keys($slots)" #[item]="data">

View File

@ -49,6 +49,7 @@ const withDefaultPlaceholder = <T extends Component>(
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明 // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType = export type ComponentType =
| 'ApiSelect' | 'ApiSelect'
| 'ApiTreeSelect'
| 'AutoComplete' | 'AutoComplete'
| 'Checkbox' | 'Checkbox'
| 'CheckboxGroup' | 'CheckboxGroup'
@ -88,7 +89,23 @@ async function initComponentAdapter() {
...attrs, ...attrs,
component: Select, component: Select,
loadingSlot: 'suffixIcon', loadingSlot: 'suffixIcon',
modelField: 'value', modelPropName: 'value',
visibleEvent: 'onVisibleChange',
},
slots,
);
},
ApiTreeSelect: (props, { attrs, slots }) => {
return h(
ApiSelect,
{
...props,
...attrs,
component: TreeSelect,
fieldNames: { label: 'label', value: 'value', children: 'children' },
loadingSlot: 'suffixIcon',
modelPropName: 'value',
optionsPropName: 'treeData',
visibleEvent: 'onVisibleChange', visibleEvent: 'onVisibleChange',
}, },
slots, slots,

View File

@ -62,6 +62,23 @@ const [BaseForm, baseFormApi] = useVbenForm({
// label // label
label: 'ApiSelect', label: 'ApiSelect',
}, },
{
component: 'ApiTreeSelect',
//
componentProps: {
//
api: getAllMenusApi,
childrenField: 'children',
// options
labelField: 'name',
placeholder: '请选择',
valueField: 'path',
},
//
fieldName: 'apiTree',
// label
label: 'ApiTreeSelect',
},
{ {
component: 'InputPassword', component: 'InputPassword',
componentProps: { componentProps: {