diff --git a/apps/web-antd/src/adapter/component/index.ts b/apps/web-antd/src/adapter/component/index.ts index c34c67ac7ec..3a4f9e5a14a 100644 --- a/apps/web-antd/src/adapter/component/index.ts +++ b/apps/web-antd/src/adapter/component/index.ts @@ -49,6 +49,7 @@ const withDefaultPlaceholder = ( // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明 export type ComponentType = | 'ApiSelect' + | 'ApiTreeSelect' | 'AutoComplete' | 'Checkbox' | 'CheckboxGroup' @@ -88,7 +89,23 @@ async function initComponentAdapter() { component: Select, loadingSlot: 'suffixIcon', 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, ); diff --git a/apps/web-ele/src/adapter/component/index.ts b/apps/web-ele/src/adapter/component/index.ts index 32d169adea4..54e10df8076 100644 --- a/apps/web-ele/src/adapter/component/index.ts +++ b/apps/web-ele/src/adapter/component/index.ts @@ -48,6 +48,7 @@ const withDefaultPlaceholder = ( // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明 export type ComponentType = | 'ApiSelect' + | 'ApiTreeSelect' | 'Checkbox' | 'CheckboxGroup' | 'DatePicker' @@ -77,7 +78,23 @@ async function initComponentAdapter() { ...attrs, component: ElSelectV2, 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, ); diff --git a/apps/web-ele/src/views/demos/form/basic.vue b/apps/web-ele/src/views/demos/form/basic.vue index 689e275fb30..484a8497e32 100644 --- a/apps/web-ele/src/views/demos/form/basic.vue +++ b/apps/web-ele/src/views/demos/form/basic.vue @@ -6,6 +6,7 @@ import { Page } from '@vben/common-ui'; import { ElButton, ElCard, ElCheckbox, ElMessage } from 'element-plus'; import { useVbenForm } from '#/adapter/form'; +import { getAllMenusApi } from '#/api'; const [Form, formApi] = useVbenForm({ commonConfig: { @@ -21,6 +22,44 @@ const [Form, formApi] = useVbenForm({ ElMessage.success(`表单数据:${JSON.stringify(values)}`); }, 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', fieldName: 'string', diff --git a/apps/web-naive/src/adapter/component/index.ts b/apps/web-naive/src/adapter/component/index.ts index a033f6f2a04..8bc56f2c903 100644 --- a/apps/web-naive/src/adapter/component/index.ts +++ b/apps/web-naive/src/adapter/component/index.ts @@ -45,6 +45,7 @@ const withDefaultPlaceholder = ( // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明 export type ComponentType = | 'ApiSelect' + | 'ApiTreeSelect' | 'Checkbox' | 'CheckboxGroup' | 'DatePicker' @@ -74,7 +75,24 @@ async function initComponentAdapter() { ...props, ...attrs, 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, ); diff --git a/apps/web-naive/src/views/demos/form/basic.vue b/apps/web-naive/src/views/demos/form/basic.vue index bc40d5b9078..2e33dfc734c 100644 --- a/apps/web-naive/src/views/demos/form/basic.vue +++ b/apps/web-naive/src/views/demos/form/basic.vue @@ -4,6 +4,7 @@ import { Page } from '@vben/common-ui'; import { NButton, NCard, useMessage } from 'naive-ui'; import { useVbenForm } from '#/adapter/form'; +import { getAllMenusApi } from '#/api'; const message = useMessage(); const [Form, formApi] = useVbenForm({ @@ -20,6 +21,44 @@ const [Form, formApi] = useVbenForm({ message.success(`表单数据:${JSON.stringify(values)}`); }, 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', fieldName: 'string', diff --git a/packages/effects/common-ui/src/components/api-select/api-select.vue b/packages/effects/common-ui/src/components/api-select/api-select.vue index fb2444b4135..71e1f125e3b 100644 --- a/packages/effects/common-ui/src/components/api-select/api-select.vue +++ b/packages/effects/common-ui/src/components/api-select/api-select.vue @@ -10,30 +10,47 @@ import { objectOmit } from '@vueuse/core'; type OptionsItem = { [name: string]: any; + children?: OptionsItem[]; disabled?: boolean; label?: string; value?: string; }; interface Props { - // 组件 + /** 组件 */ component: VNode; + /** 是否将value从数字转为string */ numberToString?: boolean; + /** 获取options数据的函数 */ api?: (arg?: any) => Promise>; + /** 传递给api的参数 */ params?: Record; + /** 从api返回的结果中提取options数组的字段名 */ resultField?: string; + /** label字段名 */ labelField?: string; + /** children字段名,需要层级数据的组件可用 */ + childrenField?: string; + /** value字段名 */ valueField?: string; + /** 组件接收options数据的属性名 */ + optionsPropName?: string; + /** 是否立即调用api */ immediate?: boolean; + /** 每次`visibleEvent`事件发生时都重新请求数据 */ alwaysLoad?: boolean; + /** 在api请求之前的回调函数 */ beforeFetch?: AnyPromiseFunction; + /** 在api请求之后的回调函数 */ afterFetch?: AnyPromiseFunction; + /** 直接传入选项数据,也作为api返回空数据时的后备数据 */ options?: OptionsItem[]; - // 尾部插槽 + /** 组件的插槽名称,用来显示一个"加载中"的图标 */ loadingSlot?: string; - // 可见时触发的事件名 + /** 触发api请求的事件名 */ visibleEvent?: string; - modelField?: string; + /** 组件的v-model属性名,默认为modelValue。部分组件可能为value */ + modelPropName?: string; } defineOptions({ name: 'ApiSelect', inheritAttrs: false }); @@ -41,6 +58,8 @@ defineOptions({ name: 'ApiSelect', inheritAttrs: false }); const props = withDefaults(defineProps(), { labelField: 'label', valueField: 'value', + childrenField: '', + optionsPropName: 'options', resultField: '', visibleEvent: '', numberToString: false, @@ -50,7 +69,7 @@ const props = withDefaults(defineProps(), { loadingSlot: '', beforeFetch: undefined, afterFetch: undefined, - modelField: 'modelValue', + modelPropName: 'modelValue', api: undefined, options: () => [], }); @@ -69,29 +88,34 @@ const loading = ref(false); const isFirstLoaded = ref(false); const getOptions = computed(() => { - const { labelField, valueField, numberToString } = props; + const { labelField, valueField, childrenField, numberToString } = props; - const data: OptionsItem[] = []; const refOptionsData = unref(refOptions); - for (const next of refOptionsData) { - if (next) { - const value = get(next, valueField); - data.push({ - ...objectOmit(next, [labelField, valueField]), - label: get(next, labelField), + function transformData(data: OptionsItem[]): OptionsItem[] { + return data.map((item) => { + const value = get(item, valueField); + return { + ...objectOmit(item, [labelField, valueField, childrenField]), + label: get(item, labelField), value: numberToString ? `${value}` : value, - }); - } + ...(childrenField && item[childrenField] + ? { children: transformData(item[childrenField]) } + : {}), + }; + }); } + const data: OptionsItem[] = transformData(refOptionsData); + return data.length > 0 ? data : props.options; }); const bindProps = computed(() => { return { - [props.modelField]: unref(modelValue), - [`onUpdate:${props.modelField}`]: (val: string) => { + [props.modelPropName]: unref(modelValue), + [props.optionsPropName]: unref(getOptions), + [`onUpdate:${props.modelPropName}`]: (val: string) => { modelValue.value = val; }, ...objectOmit(attrs, ['onUpdate:value']), @@ -168,7 +192,6 @@ function emitChange() {