From 958c8b4f21398a996fab7fb9e34f11ad954ea54b Mon Sep 17 00:00:00 2001 From: Netfan Date: Sun, 8 Dec 2024 19:23:46 +0800 Subject: [PATCH 01/17] feat: imporve naive form component (#5071) --- apps/web-naive/src/adapter/component/index.ts | 42 ++++++- .../src/locales/langs/en-US/demos.json | 1 + .../src/locales/langs/zh-CN/demos.json | 1 + .../src/router/routes/modules/demos.ts | 8 ++ apps/web-naive/src/views/demos/form/basic.vue | 106 ++++++++++++++++++ 5 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 apps/web-naive/src/views/demos/form/basic.vue diff --git a/apps/web-naive/src/adapter/component/index.ts b/apps/web-naive/src/adapter/component/index.ts index 6fa96510..a033f6f2 100644 --- a/apps/web-naive/src/adapter/component/index.ts +++ b/apps/web-naive/src/adapter/component/index.ts @@ -19,6 +19,8 @@ import { NDivider, NInput, NInputNumber, + NRadio, + NRadioButton, NRadioGroup, NSelect, NSpace, @@ -78,7 +80,22 @@ async function initComponentAdapter() { ); }, Checkbox: NCheckbox, - CheckboxGroup: NCheckboxGroup, + CheckboxGroup: (props, { attrs, slots }) => { + let defaultSlot; + if (Reflect.has(slots, 'default')) { + defaultSlot = slots.default; + } else { + const { options } = attrs; + if (Array.isArray(options)) { + defaultSlot = () => options.map((option) => h(NCheckbox, option)); + } + } + return h( + NCheckboxGroup, + { ...props, ...attrs }, + { default: defaultSlot }, + ); + }, DatePicker: NDatePicker, // 自定义默认按钮 DefaultButton: (props, { attrs, slots }) => { @@ -98,7 +115,28 @@ async function initComponentAdapter() { }, Input: withDefaultPlaceholder(NInput, 'input'), InputNumber: withDefaultPlaceholder(NInputNumber, 'input'), - RadioGroup: NRadioGroup, + RadioGroup: (props, { attrs, slots }) => { + let defaultSlot; + if (Reflect.has(slots, 'default')) { + defaultSlot = slots.default; + } else { + const { options } = attrs; + if (Array.isArray(options)) { + defaultSlot = () => + options.map((option) => + h(attrs.isButton ? NRadioButton : NRadio, option), + ); + } + } + const groupRender = h( + NRadioGroup, + { ...props, ...attrs }, + { default: defaultSlot }, + ); + return attrs.isButton + ? h(NSpace, { vertical: true }, () => groupRender) + : groupRender; + }, Select: withDefaultPlaceholder(NSelect, 'select'), Space: NSpace, Switch: NSwitch, diff --git a/apps/web-naive/src/locales/langs/en-US/demos.json b/apps/web-naive/src/locales/langs/en-US/demos.json index 9fdffc76..839fc2e6 100644 --- a/apps/web-naive/src/locales/langs/en-US/demos.json +++ b/apps/web-naive/src/locales/langs/en-US/demos.json @@ -2,6 +2,7 @@ "title": "Demos", "naive": "Naive UI", "table": "Table", + "form": "Form", "vben": { "title": "Project", "about": "About", diff --git a/apps/web-naive/src/locales/langs/zh-CN/demos.json b/apps/web-naive/src/locales/langs/zh-CN/demos.json index 79b54fa2..e0d7e616 100644 --- a/apps/web-naive/src/locales/langs/zh-CN/demos.json +++ b/apps/web-naive/src/locales/langs/zh-CN/demos.json @@ -2,6 +2,7 @@ "title": "演示", "naive": "Naive UI", "table": "Table", + "form": "表单", "vben": { "title": "项目", "about": "关于", diff --git a/apps/web-naive/src/router/routes/modules/demos.ts b/apps/web-naive/src/router/routes/modules/demos.ts index cf565880..d0631cb5 100644 --- a/apps/web-naive/src/router/routes/modules/demos.ts +++ b/apps/web-naive/src/router/routes/modules/demos.ts @@ -31,6 +31,14 @@ const routes: RouteRecordRaw[] = [ path: '/demos/table', component: () => import('#/views/demos/table/index.vue'), }, + { + meta: { + title: $t('demos.form'), + }, + name: 'Form', + path: '/demos/form', + component: () => import('#/views/demos/form/basic.vue'), + }, ], }, ]; diff --git a/apps/web-naive/src/views/demos/form/basic.vue b/apps/web-naive/src/views/demos/form/basic.vue new file mode 100644 index 00000000..bc40d5b9 --- /dev/null +++ b/apps/web-naive/src/views/demos/form/basic.vue @@ -0,0 +1,106 @@ + + From 305549e7f28541b43e8c6923ddf70f399b946e1f Mon Sep 17 00:00:00 2001 From: Netfan Date: Sun, 8 Dec 2024 19:29:49 +0800 Subject: [PATCH 02/17] feat: improve element plus form component (#5072) --- apps/web-ele/src/adapter/component/index.ts | 62 +++++++- apps/web-ele/src/adapter/form.ts | 1 + .../src/locales/langs/en-US/demos.json | 1 + .../src/locales/langs/zh-CN/demos.json | 1 + .../src/router/routes/modules/demos.ts | 8 + apps/web-ele/src/views/demos/form/basic.vue | 143 ++++++++++++++++++ 6 files changed, 213 insertions(+), 3 deletions(-) create mode 100644 apps/web-ele/src/views/demos/form/basic.vue diff --git a/apps/web-ele/src/adapter/component/index.ts b/apps/web-ele/src/adapter/component/index.ts index 451fef4a..32d169ad 100644 --- a/apps/web-ele/src/adapter/component/index.ts +++ b/apps/web-ele/src/adapter/component/index.ts @@ -15,12 +15,16 @@ import { $t } from '@vben/locales'; import { ElButton, ElCheckbox, + ElCheckboxButton, ElCheckboxGroup, ElDatePicker, ElDivider, ElInput, ElInputNumber, ElNotification, + ElOption, + ElRadio, + ElRadioButton, ElRadioGroup, ElSelect, ElSelectV2, @@ -79,7 +83,25 @@ async function initComponentAdapter() { ); }, Checkbox: ElCheckbox, - CheckboxGroup: ElCheckboxGroup, + CheckboxGroup: (props, { attrs, slots }) => { + let defaultSlot; + if (Reflect.has(slots, 'default')) { + defaultSlot = slots.default; + } else { + const { options, isButton } = attrs; + if (Array.isArray(options)) { + defaultSlot = () => + options.map((option) => + h(isButton ? ElCheckboxButton : ElCheckbox, option), + ); + } + } + return h( + ElCheckboxGroup, + { ...props, ...attrs }, + { ...slots, default: defaultSlot }, + ); + }, // 自定义默认按钮 DefaultButton: (props, { attrs, slots }) => { return h(ElButton, { ...props, attrs, type: 'info' }, slots); @@ -104,8 +126,42 @@ async function initComponentAdapter() { }, Input: withDefaultPlaceholder(ElInput, 'input'), InputNumber: withDefaultPlaceholder(ElInputNumber, 'input'), - RadioGroup: ElRadioGroup, - Select: withDefaultPlaceholder(ElSelect, 'select'), + RadioGroup: (props, { attrs, slots }) => { + let defaultSlot; + if (Reflect.has(slots, 'default')) { + defaultSlot = slots.default; + } else { + const { options } = attrs; + if (Array.isArray(options)) { + defaultSlot = () => + options.map((option) => + h(attrs.isButton ? ElRadioButton : ElRadio, option), + ); + } + } + return h( + ElRadioGroup, + { ...props, ...attrs }, + { ...slots, default: defaultSlot }, + ); + }, + Select: (props, { attrs, slots }) => { + let defaultSlot; + if (Reflect.has(slots, 'default')) { + defaultSlot = slots.default; + } else { + const { options } = attrs; + if (Array.isArray(options)) { + defaultSlot = () => options.map((option) => h(ElOption, option)); + } + } + const placeholder = props?.placeholder || $t(`ui.placeholder.select`); + return h( + ElSelect, + { ...props, ...attrs, placeholder }, + { ...slots, default: defaultSlot }, + ); + }, Space: ElSpace, Switch: ElSwitch, TimePicker: (props, { attrs, slots }) => { diff --git a/apps/web-ele/src/adapter/form.ts b/apps/web-ele/src/adapter/form.ts index 1b6e0471..13ae9c42 100644 --- a/apps/web-ele/src/adapter/form.ts +++ b/apps/web-ele/src/adapter/form.ts @@ -12,6 +12,7 @@ setupVbenForm({ config: { modelPropNameMap: { Upload: 'fileList', + CheckboxGroup: 'model-value', }, }, defineRules: { diff --git a/apps/web-ele/src/locales/langs/en-US/demos.json b/apps/web-ele/src/locales/langs/en-US/demos.json index 056da0da..6eddebb5 100644 --- a/apps/web-ele/src/locales/langs/en-US/demos.json +++ b/apps/web-ele/src/locales/langs/en-US/demos.json @@ -1,6 +1,7 @@ { "title": "Demos", "elementPlus": "Element Plus", + "form": "Form", "vben": { "title": "Project", "about": "About", diff --git a/apps/web-ele/src/locales/langs/zh-CN/demos.json b/apps/web-ele/src/locales/langs/zh-CN/demos.json index 0620e16a..ba6d6ccd 100644 --- a/apps/web-ele/src/locales/langs/zh-CN/demos.json +++ b/apps/web-ele/src/locales/langs/zh-CN/demos.json @@ -1,6 +1,7 @@ { "title": "演示", "elementPlus": "Element Plus", + "form": "表单演示", "vben": { "title": "项目", "about": "关于", diff --git a/apps/web-ele/src/router/routes/modules/demos.ts b/apps/web-ele/src/router/routes/modules/demos.ts index 223efcf9..90cc2f11 100644 --- a/apps/web-ele/src/router/routes/modules/demos.ts +++ b/apps/web-ele/src/router/routes/modules/demos.ts @@ -23,6 +23,14 @@ const routes: RouteRecordRaw[] = [ path: '/demos/element', component: () => import('#/views/demos/element/index.vue'), }, + { + meta: { + title: $t('demos.form'), + }, + name: 'BasicForm', + path: '/demos/form', + component: () => import('#/views/demos/form/basic.vue'), + }, ], }, ]; diff --git a/apps/web-ele/src/views/demos/form/basic.vue b/apps/web-ele/src/views/demos/form/basic.vue new file mode 100644 index 00000000..689e275f --- /dev/null +++ b/apps/web-ele/src/views/demos/form/basic.vue @@ -0,0 +1,143 @@ + + From d085736bac2bcfb5b6fd0083e65762dcedbff0b6 Mon Sep 17 00:00:00 2001 From: Netfan Date: Mon, 9 Dec 2024 12:47:33 +0800 Subject: [PATCH 03/17] feat: improve `ApiSelect` component (#5075) * feat: improve `ApiSelect` component * chore: `ApiSelect` props name changed --- apps/web-antd/src/adapter/component/index.ts | 19 +++++- apps/web-ele/src/adapter/component/index.ts | 19 +++++- apps/web-ele/src/views/demos/form/basic.vue | 39 ++++++++++++ apps/web-naive/src/adapter/component/index.ts | 20 ++++++- apps/web-naive/src/views/demos/form/basic.vue | 39 ++++++++++++ .../src/components/api-select/api-select.vue | 59 +++++++++++++------ playground/src/adapter/component/index.ts | 19 +++++- playground/src/views/examples/form/basic.vue | 17 ++++++ 8 files changed, 209 insertions(+), 22 deletions(-) diff --git a/apps/web-antd/src/adapter/component/index.ts b/apps/web-antd/src/adapter/component/index.ts index c34c67ac..3a4f9e5a 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 32d169ad..54e10df8 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 689e275f..484a8497 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 a033f6f2..8bc56f2c 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 bc40d5b9..2e33dfc7 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 fb2444b4..71e1f125 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() {