diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a34d90a..834f8252 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +# 1.1.4 + +**Features** + +- 通用的vxe-table排序事件(排序逻辑改为在排序事件中处理而非在api处理) + +**BUG FIXES** + +- 字典项为空时getDict方法无限调用接口((无奈兼容 不给字典item本来就是错误用法)) + # 1.1.3 **REFACTOR** diff --git a/apps/web-antd/src/adapter/component/index.ts b/apps/web-antd/src/adapter/component/index.ts index e882435d..370bcdf3 100644 --- a/apps/web-antd/src/adapter/component/index.ts +++ b/apps/web-antd/src/adapter/component/index.ts @@ -8,7 +8,7 @@ import type { BaseFormComponentType } from '@vben/common-ui'; import type { Component, SetupContext } from 'vue'; import { h } from 'vue'; -import { ApiSelect, globalShareState, IconPicker } from '@vben/common-ui'; +import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui'; import { $t } from '@vben/locales'; import { @@ -52,6 +52,7 @@ const withDefaultPlaceholder = ( // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明 export type ComponentType = | 'ApiSelect' + | 'ApiTreeSelect' | 'AutoComplete' | 'Checkbox' | 'CheckboxGroup' @@ -87,14 +88,32 @@ async function initComponentAdapter() { // import('xxx').then((res) => res.Button), ApiSelect: (props, { attrs, slots }) => { return h( - ApiSelect, + ApiComponent, { + placeholder: $t('ui.placeholder.select'), ...props, ...attrs, component: Select, loadingSlot: 'suffixIcon', visibleEvent: 'onDropdownVisibleChange', - modelField: 'value', + modelPropName: 'value', + }, + slots, + ); + }, + ApiTreeSelect: (props, { attrs, slots }) => { + return h( + ApiComponent, + { + placeholder: $t('ui.placeholder.select'), + ...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-antd/src/adapter/vxe-table.ts b/apps/web-antd/src/adapter/vxe-table.ts index e8735c90..857751b3 100644 --- a/apps/web-antd/src/adapter/vxe-table.ts +++ b/apps/web-antd/src/adapter/vxe-table.ts @@ -130,3 +130,24 @@ export function vxeCheckboxChecked( ) { return tableApi?.grid?.getCheckboxRecords?.()?.length > 0; } + +/** + * 通用的vxe-table排序事件 支持单/多字段排序 + * @param tableApi api + * @param sortParams 排序参数 + */ +export function vxeSortEvent( + tableApi: ReturnType[1], + sortParams: VxeGridDefines.SortChangeEventParams, +) { + const { sortList } = sortParams; + // 这里是排序取消 length为0 就不传参数了 + if (sortList.length === 0) { + tableApi.query(); + return; + } + // 支持单/多字段排序 + const orderByColumn = sortList.map((item) => item.field).join(','); + const isAsc = sortList.map((item) => item.order).join(','); + tableApi.query({ orderByColumn, isAsc }); +} diff --git a/apps/web-antd/src/api/common.d.ts b/apps/web-antd/src/api/common.d.ts index a8c520d9..233f2aa6 100644 --- a/apps/web-antd/src/api/common.d.ts +++ b/apps/web-antd/src/api/common.d.ts @@ -21,13 +21,20 @@ export interface PageResult { /** * 分页查询参数 + * + * 排序支持的用法如下: + * {isAsc:"asc",orderByColumn:"id"} order by id asc + * {isAsc:"asc",orderByColumn:"id,createTime"} order by id asc,create_time asc + * {isAsc:"desc",orderByColumn:"id,createTime"} order by id desc,create_time desc + * {isAsc:"asc,desc",orderByColumn:"id,createTime"} order by id asc,create_time desc + * * @param pageNum 当前页 * @param pageSize 每页大小 * @param orderByColumn 排序字段 * @param isAsc 是否升序 */ export interface PageQuery { - isAsc?: boolean; + isAsc?: string; orderByColumn?: string; pageNum?: number; pageSize?: number; diff --git a/apps/web-antd/src/utils/dict.ts b/apps/web-antd/src/utils/dict.ts index 9e4388c0..ce108d1f 100644 --- a/apps/web-antd/src/utils/dict.ts +++ b/apps/web-antd/src/utils/dict.ts @@ -20,7 +20,14 @@ export function getDict(dictName: string): DictData[] { }) .finally(() => { // 移除请求状态缓存 - dictRequestCache.delete(dictName); + /** + * 这里主要判断字典item为空的情况(无奈兼容 不给字典item本来就是错误用法) + * 会导致if一直进入逻辑导致接口无限刷新 + * 在这里dictList为空时 不删除缓存 + */ + if (dictList.length > 0) { + dictRequestCache.delete(dictName); + } }), ); } @@ -42,7 +49,14 @@ export function getDictOptions(dictName: string): Option[] { }) .finally(() => { // 移除请求状态缓存 - dictRequestCache.delete(dictName); + /** + * 这里主要判断字典item为空的情况(无奈兼容 不给字典item本来就是错误用法) + * 会导致if一直进入逻辑导致接口五线刷新 + * 在这里dictList为空时 不删除缓存 + */ + if (dictOptionList.length > 0) { + dictRequestCache.delete(dictName); + } }), ); } diff --git a/apps/web-antd/src/views/monitor/operlog/index.vue b/apps/web-antd/src/views/monitor/operlog/index.vue index da7e126b..74c34dd5 100644 --- a/apps/web-antd/src/views/monitor/operlog/index.vue +++ b/apps/web-antd/src/views/monitor/operlog/index.vue @@ -7,12 +7,12 @@ import { Page, useVbenDrawer, type VbenFormProps } from '@vben/common-ui'; import { $t } from '@vben/locales'; import { Modal, Space } from 'ant-design-vue'; -import { isEmpty } from 'lodash-es'; import { useVbenVxeGrid, vxeCheckboxChecked, type VxeGridProps, + vxeSortEvent, } from '#/adapter/vxe-table'; import { operLogClean, @@ -60,18 +60,12 @@ const gridOptions: VxeGridProps = { pagerConfig: {}, proxyConfig: { ajax: { - query: async ({ page, sort }, formValues = {}) => { + query: async ({ page }, formValues = {}) => { const params: any = { pageNum: page.currentPage, pageSize: page.pageSize, ...formValues, }; - - if (!isEmpty(sort)) { - params.orderByColumn = sort.field; - params.isAsc = sort.order; - } - return await operLogList(params); }, }, @@ -81,7 +75,10 @@ const gridOptions: VxeGridProps = { keyField: 'operId', }, sortConfig: { + // 远程排序 remote: true, + // 支持多字段排序 默认关闭 + multiple: true, }, id: 'monitor-operlog-index', }; @@ -90,9 +87,7 @@ const [BasicTable, tableApi] = useVbenVxeGrid({ formOptions, gridOptions, gridEvents: { - sortChange: () => { - tableApi.query(); - }, + sortChange: (sortParams) => vxeSortEvent(tableApi, sortParams), }, }); diff --git a/apps/web-antd/src/views/system/oss/index.vue b/apps/web-antd/src/views/system/oss/index.vue index 958daf03..eebccedf 100644 --- a/apps/web-antd/src/views/system/oss/index.vue +++ b/apps/web-antd/src/views/system/oss/index.vue @@ -17,12 +17,12 @@ import { Switch, Tooltip, } from 'ant-design-vue'; -import { isEmpty } from 'lodash-es'; import { useVbenVxeGrid, vxeCheckboxChecked, type VxeGridProps, + vxeSortEvent, } from '#/adapter/vxe-table'; import { configInfoByKey } from '#/api/system/config'; import { ossDownload, ossList, ossRemove } from '#/api/system/oss'; @@ -66,16 +66,12 @@ const gridOptions: VxeGridProps = { pagerConfig: {}, proxyConfig: { ajax: { - query: async ({ page, sort }, formValues = {}) => { + query: async ({ page }, formValues = {}) => { const params: any = { pageNum: page.currentPage, pageSize: page.pageSize, ...formValues, }; - if (!isEmpty(sort)) { - params.orderByColumn = sort.field; - params.isAsc = sort.order; - } return await ossList(params); }, }, @@ -86,7 +82,10 @@ const gridOptions: VxeGridProps = { height: 65, }, sortConfig: { + // 远程排序 remote: true, + // 支持多字段排序 默认关闭 + multiple: false, }, id: 'system-oss-index', }; @@ -95,9 +94,7 @@ const [BasicTable, tableApi] = useVbenVxeGrid({ formOptions, gridOptions, gridEvents: { - sortChange: () => { - tableApi.query(); - }, + sortChange: (sortParams) => vxeSortEvent(tableApi, sortParams), }, }); diff --git a/apps/web-antd/src/views/system/user/index.vue b/apps/web-antd/src/views/system/user/index.vue index b361c048..24b0ed13 100644 --- a/apps/web-antd/src/views/system/user/index.vue +++ b/apps/web-antd/src/views/system/user/index.vue @@ -192,7 +192,7 @@ const { hasAccessByCodes } = useAccess();
diff --git a/apps/web-ele/src/adapter/component/index.ts b/apps/web-ele/src/adapter/component/index.ts index 451fef4a..818c8c4e 100644 --- a/apps/web-ele/src/adapter/component/index.ts +++ b/apps/web-ele/src/adapter/component/index.ts @@ -9,20 +9,22 @@ import type { Recordable } from '@vben/types'; import type { Component, SetupContext } from 'vue'; import { h } from 'vue'; -import { ApiSelect, globalShareState, IconPicker } from '@vben/common-ui'; +import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui'; import { $t } from '@vben/locales'; import { ElButton, ElCheckbox, + ElCheckboxButton, ElCheckboxGroup, ElDatePicker, ElDivider, ElInput, ElInputNumber, ElNotification, + ElRadio, + ElRadioButton, ElRadioGroup, - ElSelect, ElSelectV2, ElSpace, ElSwitch, @@ -44,6 +46,7 @@ const withDefaultPlaceholder = ( // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明 export type ComponentType = | 'ApiSelect' + | 'ApiTreeSelect' | 'Checkbox' | 'CheckboxGroup' | 'DatePicker' @@ -67,19 +70,55 @@ async function initComponentAdapter() { // import('xxx').then((res) => res.Button), ApiSelect: (props, { attrs, slots }) => { return h( - ApiSelect, + ApiComponent, { + placeholder: $t('ui.placeholder.select'), ...props, ...attrs, component: ElSelectV2, loadingSlot: 'loading', - visibleEvent: 'onDropdownVisibleChange', + visibleEvent: 'onVisibleChange', + }, + slots, + ); + }, + ApiTreeSelect: (props, { attrs, slots }) => { + return h( + ApiComponent, + { + placeholder: $t('ui.placeholder.select'), + ...props, + ...attrs, + component: ElTreeSelect, + props: { label: 'label', children: 'children' }, + nodeKey: 'value', + loadingSlot: 'loading', + optionsPropName: 'data', + visibleEvent: 'onVisibleChange', }, slots, ); }, 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 +143,28 @@ 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 }) => { + return h(ElSelectV2, { ...props, attrs }, slots); + }, 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..90aaccef --- /dev/null +++ b/apps/web-ele/src/views/demos/form/basic.vue @@ -0,0 +1,180 @@ + + diff --git a/apps/web-naive/src/adapter/component/index.ts b/apps/web-naive/src/adapter/component/index.ts index 6fa96510..545a6199 100644 --- a/apps/web-naive/src/adapter/component/index.ts +++ b/apps/web-naive/src/adapter/component/index.ts @@ -8,7 +8,7 @@ import type { BaseFormComponentType } from '@vben/common-ui'; import type { Component, SetupContext } from 'vue'; import { h } from 'vue'; -import { ApiSelect, globalShareState, IconPicker } from '@vben/common-ui'; +import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui'; import { $t } from '@vben/locales'; import { @@ -19,6 +19,8 @@ import { NDivider, NInput, NInputNumber, + NRadio, + NRadioButton, NRadioGroup, NSelect, NSpace, @@ -43,6 +45,7 @@ const withDefaultPlaceholder = ( // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明 export type ComponentType = | 'ApiSelect' + | 'ApiTreeSelect' | 'Checkbox' | 'CheckboxGroup' | 'DatePicker' @@ -67,18 +70,52 @@ async function initComponentAdapter() { ApiSelect: (props, { attrs, slots }) => { return h( - ApiSelect, + ApiComponent, { + placeholder: $t('ui.placeholder.select'), ...props, ...attrs, component: NSelect, - modelField: 'value', + modelPropName: 'value', + }, + slots, + ); + }, + ApiTreeSelect: (props, { attrs, slots }) => { + return h( + ApiComponent, + { + placeholder: $t('ui.placeholder.select'), + ...props, + ...attrs, + component: NTreeSelect, + nodeKey: 'value', + loadingSlot: 'arrow', + keyField: 'value', + modelPropName: 'value', + optionsPropName: 'options', + visibleEvent: 'onVisibleChange', }, slots, ); }, 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 +135,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..7d04ff4d --- /dev/null +++ b/apps/web-naive/src/views/demos/form/basic.vue @@ -0,0 +1,143 @@ + + diff --git a/docs/.vitepress/config/zh.mts b/docs/.vitepress/config/zh.mts index 27fb96c0..25e93ced 100644 --- a/docs/.vitepress/config/zh.mts +++ b/docs/.vitepress/config/zh.mts @@ -162,6 +162,10 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] { collapsed: false, text: '通用组件', items: [ + { + link: 'common-ui/vben-api-component', + text: 'ApiComponent Api组件包装器', + }, { link: 'common-ui/vben-modal', text: 'Modal 模态框', diff --git a/docs/src/components/common-ui/vben-api-component.md b/docs/src/components/common-ui/vben-api-component.md new file mode 100644 index 00000000..f9db74e4 --- /dev/null +++ b/docs/src/components/common-ui/vben-api-component.md @@ -0,0 +1,150 @@ +--- +outline: deep +--- + +# Vben ApiComponent Api组件包装器 + +框架提供的API“包装器”,它一般不独立使用,主要用于包装其它组件,为目标组件提供自动获取远程数据的能力,但仍然保持了目标组件的原始用法。 + +::: info 写在前面 + +我们在各个应用的组件适配器中,使用ApiComponent包装了Select、TreeSelect组件,使得这些组件可以自动获取远程数据并生成选项。其它类似的组件(比如Cascader)如有需要也可以参考示例代码自行进行包装。 + +::: + +## 基础用法 + +通过 `component` 传入其它组件的定义,并配置相关的其它属性(主要是一些名称映射)。包装组件将通过`api`获取数据(`beforerFetch`、`afterFetch`将分别在`api`运行前、运行后被调用),使用`resultField`从中提取数组,使用`valueField`、`labelField`等来从数据中提取value和label(如果提供了`childrenField`,会将其作为树形结构递归处理每一级数据),之后将处理好的数据通过`optionsPropName`指定的属性传递给目标组件。 + +::: details 包装级联选择器,点击下拉时开始加载远程数据 + +```vue + + +``` + +::: + +### Props + +| 属性名 | 描述 | 类型 | 默认值 | +| --- | --- | --- | --- | +| component | 欲包装的组件 | `Component` | - | +| numberToString | 是否将value从数字转为string | `boolean` | `false` | +| api | 获取数据的函数 | `(arg?: any) => Promise>` | - | +| params | 传递给api的参数 | `Record` | - | +| resultField | 从api返回的结果中提取options数组的字段名 | `string` | - | +| labelField | label字段名 | `string` | `label` | +| childrenField | 子级数据字段名,需要层级数据的组件可用 | `string` | `` | +| valueField | value字段名 | `string` | `value` | +| optionsPropName | 组件接收options数据的属性名称 | `string` | `options` | +| modelPropName | 组件的双向绑定属性名,默认为modelValue。部分组件可能为value | `string` | `modelValue` | +| immediate | 是否立即调用api | `boolean` | `true` | +| alwaysLoad | 每次`visibleEvent`事件发生时都重新请求数据 | `boolean` | `false` | +| beforeFetch | 在api请求之前的回调函数 | `AnyPromiseFunction` | - | +| afterFetch | 在api请求之后的回调函数 | `AnyPromiseFunction` | - | +| options | 直接传入选项数据,也作为api返回空数据时的后备数据 | `OptionsItem[]` | - | +| visibleEvent | 触发重新请求数据的事件名 | `string` | - | +| loadingSlot | 组件的插槽名称,用来显示一个"加载中"的图标 | `string` | - | + +``` + +``` diff --git a/docs/src/components/common-ui/vben-drawer.md b/docs/src/components/common-ui/vben-drawer.md index 939593fa..7091570f 100644 --- a/docs/src/components/common-ui/vben-drawer.md +++ b/docs/src/components/common-ui/vben-drawer.md @@ -74,6 +74,7 @@ const [Drawer, drawerApi] = useVbenDrawer({ | 属性名 | 描述 | 类型 | 默认值 | | --- | --- | --- | --- | +| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` | | title | 标题 | `string\|slot` | - | | titleTooltip | 标题提示信息 | `string\|slot` | - | | description | 描述信息 | `string\|slot` | - | @@ -95,6 +96,13 @@ const [Drawer, drawerApi] = useVbenDrawer({ | contentClass | modal内容区域的class | `string` | - | | footerClass | modal底部区域的class | `string` | - | | headerClass | modal顶部区域的class | `string` | - | +| zIndex | 抽屉的ZIndex层级 | `number` | `1000` | + +::: info appendToMain + +`appendToMain`可以指定将抽屉挂载到内容区域,打开抽屉时,内容区域以外的部分(标签栏、导航菜单等等)不会被遮挡。默认情况下,抽屉会挂载到body上。但是:挂载到内容区域时,作为页面根容器的`Page`组件,需要设置`auto-content-height`属性,以便抽屉能够正确计算高度。 + +::: ### Event diff --git a/docs/src/components/common-ui/vben-form.md b/docs/src/components/common-ui/vben-form.md index ce27e1f2..618e3d3a 100644 --- a/docs/src/components/common-ui/vben-form.md +++ b/docs/src/components/common-ui/vben-form.md @@ -306,6 +306,8 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单 | actionWrapperClass | 表单操作区域class | `any` | - | | handleReset | 表单重置回调 | `(values: Record,) => Promise \| void` | - | | handleSubmit | 表单提交回调 | `(values: Record,) => Promise \| void` | - | +| handleValuesChange | 表单值变化回调 | `(values: Record,) => void` | - | +| actionButtonsReverse | 调换操作按钮位置 | `boolean` | `false` | | resetButtonOptions | 重置按钮组件参数 | `ActionButtonOptions` | - | | submitButtonOptions | 提交按钮组件参数 | `ActionButtonOptions` | - | | showDefaultActions | 是否显示默认操作按钮 | `boolean` | `true` | diff --git a/docs/src/components/common-ui/vben-modal.md b/docs/src/components/common-ui/vben-modal.md index 75f620ae..d6e3ef47 100644 --- a/docs/src/components/common-ui/vben-modal.md +++ b/docs/src/components/common-ui/vben-modal.md @@ -80,6 +80,7 @@ const [Modal, modalApi] = useVbenModal({ | 属性名 | 描述 | 类型 | 默认值 | | --- | --- | --- | --- | +| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` | | title | 标题 | `string\|slot` | - | | titleTooltip | 标题提示信息 | `string\|slot` | - | | description | 描述信息 | `string\|slot` | - | @@ -106,6 +107,13 @@ const [Modal, modalApi] = useVbenModal({ | footerClass | modal底部区域的class | `string` | - | | headerClass | modal顶部区域的class | `string` | - | | bordered | 是否显示border | `boolean` | `false` | +| zIndex | 弹窗的ZIndex层级 | `number` | `1000` | + +::: info appendToMain + +`appendToMain`可以指定将弹窗挂载到内容区域,打开这种弹窗时,内容区域以外的部分(标签栏、导航菜单等等)不会被遮挡。默认情况下,弹窗会挂载到body上。但是:挂载到内容区域时,作为页面根容器的`Page`组件,需要设置`auto-content-height`属性,以便弹窗能够正确计算高度。 + +::: ### Event diff --git a/docs/src/demos/vben-api-component/cascader/index.vue b/docs/src/demos/vben-api-component/cascader/index.vue new file mode 100644 index 00000000..957964cd --- /dev/null +++ b/docs/src/demos/vben-api-component/cascader/index.vue @@ -0,0 +1,100 @@ + + diff --git a/package.json b/package.json index ba4d96a0..a5501e58 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "node": ">=20.10.0", "pnpm": ">=9.12.0" }, - "packageManager": "pnpm@9.14.4", + "packageManager": "pnpm@9.15.0", "pnpm": { "peerDependencyRules": { "allowedVersions": { diff --git a/packages/@core/base/shared/src/constants/globals.ts b/packages/@core/base/shared/src/constants/globals.ts index 17941de1..3c699570 100644 --- a/packages/@core/base/shared/src/constants/globals.ts +++ b/packages/@core/base/shared/src/constants/globals.ts @@ -7,6 +7,9 @@ export const CSS_VARIABLE_LAYOUT_HEADER_HEIGHT = `--vben-header-height`; /** layout footer 组件的高度 */ export const CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT = `--vben-footer-height`; +/** 内容区域的组件ID */ +export const ELEMENT_ID_MAIN_CONTENT = `__vben_main_content`; + /** * @zh_CN 默认命名空间 */ diff --git a/packages/@core/ui-kit/form-ui/src/components/form-actions.vue b/packages/@core/ui-kit/form-ui/src/components/form-actions.vue index 26e426fe..ac5505d5 100644 --- a/packages/@core/ui-kit/form-ui/src/components/form-actions.vue +++ b/packages/@core/ui-kit/form-ui/src/components/form-actions.vue @@ -142,13 +142,29 @@ defineExpose({ " :style="queryFormStyle" > + + - - + diff --git a/packages/@core/ui-kit/form-ui/src/types.ts b/packages/@core/ui-kit/form-ui/src/types.ts index 30daaee2..2f8a7be7 100644 --- a/packages/@core/ui-kit/form-ui/src/types.ts +++ b/packages/@core/ui-kit/form-ui/src/types.ts @@ -307,6 +307,10 @@ export interface VbenFormProps< FormRenderProps, 'componentBindEventMap' | 'componentMap' | 'form' > { + /** + * 操作按钮是否反转(提交按钮前置) + */ + actionButtonsReverse?: boolean; /** * 表单操作区域class */ diff --git a/packages/@core/ui-kit/form-ui/src/vben-use-form.vue b/packages/@core/ui-kit/form-ui/src/vben-use-form.vue index a1395328..855472d9 100644 --- a/packages/@core/ui-kit/form-ui/src/vben-use-form.vue +++ b/packages/@core/ui-kit/form-ui/src/vben-use-form.vue @@ -62,9 +62,7 @@ function handleKeyDownEnter(event: KeyboardEvent) { watch( () => form.values, useDebounceFn(() => { - (props.handleValuesChange ?? state.value.handleValuesChange)?.( - toRaw(form.values), - ); + forward.value.handleValuesChange?.(toRaw(form.values)); state.value.submitOnChange && props.formApi?.submitForm(); }, 300), { deep: true }, diff --git a/packages/@core/ui-kit/layout-ui/package.json b/packages/@core/ui-kit/layout-ui/package.json index 1039a2fa..d249146f 100644 --- a/packages/@core/ui-kit/layout-ui/package.json +++ b/packages/@core/ui-kit/layout-ui/package.json @@ -40,6 +40,7 @@ "@vben-core/composables": "workspace:*", "@vben-core/icons": "workspace:*", "@vben-core/shadcn-ui": "workspace:*", + "@vben-core/shared": "workspace:*", "@vben-core/typings": "workspace:*", "@vueuse/core": "catalog:", "vue": "catalog:" diff --git a/packages/@core/ui-kit/layout-ui/src/vben-layout.vue b/packages/@core/ui-kit/layout-ui/src/vben-layout.vue index a598f291..fe9b1d85 100644 --- a/packages/@core/ui-kit/layout-ui/src/vben-layout.vue +++ b/packages/@core/ui-kit/layout-ui/src/vben-layout.vue @@ -11,6 +11,7 @@ import { } from '@vben-core/composables'; import { Menu } from '@vben-core/icons'; import { VbenIconButton } from '@vben-core/shadcn-ui'; +import { ELEMENT_ID_MAIN_CONTENT } from '@vben-core/shared/constants'; import { useMouse, useScroll, useThrottleFn } from '@vueuse/core'; @@ -457,6 +458,8 @@ function handleHeaderToggle() { emit('toggleSidebar'); } } + +const idMainContent = ELEMENT_ID_MAIN_CONTENT;