This commit is contained in:
dap 2024-12-13 08:36:45 +08:00
commit b0d44b07b4
57 changed files with 1240 additions and 135 deletions

View File

@ -1,3 +1,13 @@
# 1.1.4
**Features**
- 通用的vxe-table排序事件(排序逻辑改为在排序事件中处理而非在api处理)
**BUG FIXES**
- 字典项为空时getDict方法无限调用接口((无奈兼容 不给字典item本来就是错误用法))
# 1.1.3
**REFACTOR**

View File

@ -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 = <T extends Component>(
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
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,
);

View File

@ -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<typeof useVbenVxeGrid>[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 });
}

View File

@ -21,13 +21,20 @@ export interface PageResult<T = any> {
/**
*
*
* :
* {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;

View File

@ -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);
}
}),
);
}

View File

@ -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<OperationLog> = {
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<OperationLog> = {
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),
},
});

View File

@ -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),
},
});

View File

@ -192,7 +192,7 @@ const { hasAccessByCodes } = useAccess();
<div class="flex h-full gap-[8px]">
<DeptTree
v-model:select-dept-id="selectDeptId"
:width="260"
class="w-[260px]"
@reload="() => tableApi.reload()"
@select="() => tableApi.reload()"
/>

View File

@ -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 = <T extends Component>(
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
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 }) => {

View File

@ -12,6 +12,7 @@ setupVbenForm<ComponentType>({
config: {
modelPropNameMap: {
Upload: 'fileList',
CheckboxGroup: 'model-value',
},
},
defineRules: {

View File

@ -1,6 +1,7 @@
{
"title": "Demos",
"elementPlus": "Element Plus",
"form": "Form",
"vben": {
"title": "Project",
"about": "About",

View File

@ -1,6 +1,7 @@
{
"title": "演示",
"elementPlus": "Element Plus",
"form": "表单演示",
"vben": {
"title": "项目",
"about": "关于",

View File

@ -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'),
},
],
},
];

View File

@ -0,0 +1,180 @@
<script lang="ts" setup>
import { h } from 'vue';
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: {
//
componentProps: {
class: 'w-full',
},
},
layout: 'horizontal',
// 321
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
handleSubmit: (values) => {
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,
},
//
fieldName: 'api',
// label
label: 'ApiSelect',
},
{
component: 'ApiTreeSelect',
//
componentProps: {
//
api: getAllMenusApi,
childrenField: 'children',
// options
labelField: 'name',
valueField: 'path',
},
//
fieldName: 'apiTree',
// label
label: 'ApiTreeSelect',
},
{
component: 'Input',
fieldName: 'string',
label: 'String',
},
{
component: 'InputNumber',
fieldName: 'number',
label: 'Number',
},
{
component: 'RadioGroup',
fieldName: 'radio',
label: 'Radio',
componentProps: {
options: [
{ value: 'A', label: 'A' },
{ value: 'B', label: 'B' },
{ value: 'C', label: 'C' },
{ value: 'D', label: 'D' },
{ value: 'E', label: 'E' },
],
},
},
{
component: 'RadioGroup',
fieldName: 'radioButton',
label: 'RadioButton',
componentProps: {
isButton: true,
options: ['A', 'B', 'C', 'D', 'E', 'F'].map((v) => ({
value: v,
label: `选项${v}`,
})),
},
},
{
component: 'CheckboxGroup',
fieldName: 'checkbox',
label: 'Checkbox',
componentProps: {
options: ['A', 'B', 'C'].map((v) => ({ value: v, label: `选项${v}` })),
},
},
{
component: 'CheckboxGroup',
fieldName: 'checkbox1',
label: 'Checkbox1',
renderComponentContent: () => {
return {
default: () => {
return ['A', 'B', 'C', 'D'].map((v) =>
h(ElCheckbox, { label: v, value: v }),
);
},
};
},
},
{
component: 'CheckboxGroup',
fieldName: 'checkbotton',
label: 'CheckBotton',
componentProps: {
isButton: true,
options: [
{ value: 'A', label: '选项A' },
{ value: 'B', label: '选项B' },
{ value: 'C', label: '选项C' },
],
},
},
{
component: 'DatePicker',
fieldName: 'date',
label: 'Date',
},
{
component: 'Select',
fieldName: 'select',
label: 'Select',
componentProps: {
options: [
{ value: 'A', label: '选项A' },
{ value: 'B', label: '选项B' },
{ value: 'C', label: '选项C' },
],
},
},
],
});
function setFormValues() {
formApi.setValues({
string: 'string',
number: 123,
radio: 'B',
radioButton: 'C',
checkbox: ['A', 'C'],
checkbotton: ['B', 'C'],
checkbox1: ['A', 'B'],
date: new Date(),
select: 'B',
});
}
</script>
<template>
<Page
description="我们重新包装了CheckboxGroup、RadioGroup、Select可以通过options属性传入选项属性数组以自动生成选项"
title="表单演示"
>
<ElCard>
<template #header>
<div class="flex items-center">
<span class="flex-auto">基础表单演示</span>
<ElButton type="primary" @click="setFormValues">设置表单值</ElButton>
</div>
</template>
<Form />
</ElCard>
</Page>
</template>

View File

@ -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 = <T extends Component>(
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
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,

View File

@ -2,6 +2,7 @@
"title": "Demos",
"naive": "Naive UI",
"table": "Table",
"form": "Form",
"vben": {
"title": "Project",
"about": "About",

View File

@ -2,6 +2,7 @@
"title": "演示",
"naive": "Naive UI",
"table": "Table",
"form": "表单",
"vben": {
"title": "项目",
"about": "关于",

View File

@ -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'),
},
],
},
];

View File

@ -0,0 +1,143 @@
<script lang="ts" setup>
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({
commonConfig: {
//
componentProps: {
class: 'w-full',
},
},
layout: 'horizontal',
// 321
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
handleSubmit: (values) => {
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,
},
//
fieldName: 'api',
// label
label: 'ApiSelect',
},
{
component: 'ApiTreeSelect',
//
componentProps: {
//
api: getAllMenusApi,
childrenField: 'children',
// options
labelField: 'name',
valueField: 'path',
},
//
fieldName: 'apiTree',
// label
label: 'ApiTreeSelect',
},
{
component: 'Input',
fieldName: 'string',
label: 'String',
},
{
component: 'InputNumber',
fieldName: 'number',
label: 'Number',
},
{
component: 'RadioGroup',
fieldName: 'radio',
label: 'Radio',
componentProps: {
options: [
{ value: 'A', label: 'A' },
{ value: 'B', label: 'B' },
{ value: 'C', label: 'C' },
{ value: 'D', label: 'D' },
{ value: 'E', label: 'E' },
],
},
},
{
component: 'RadioGroup',
fieldName: 'radioButton',
label: 'RadioButton',
componentProps: {
isButton: true,
class: 'flex flex-wrap', // class
options: [
{ value: 'A', label: '选项A' },
{ value: 'B', label: '选项B' },
{ value: 'C', label: '选项C' },
{ value: 'D', label: '选项D' },
{ value: 'E', label: '选项E' },
{ value: 'F', label: '选项F' },
],
},
},
{
component: 'CheckboxGroup',
fieldName: 'checkbox',
label: 'Checkbox',
componentProps: {
options: [
{ value: 'A', label: '选项A' },
{ value: 'B', label: '选项B' },
{ value: 'C', label: '选项C' },
],
},
},
{
component: 'DatePicker',
fieldName: 'date',
label: 'Date',
},
],
});
function setFormValues() {
formApi.setValues({
string: 'string',
number: 123,
radio: 'B',
radioButton: 'C',
checkbox: ['A', 'C'],
date: Date.now(),
});
}
</script>
<template>
<Page
description="表单适配器重新包装了CheckboxGroup和RadioGroup可以通过options属性传递选项数据选项数据将作为子组件的属性"
title="表单演示"
>
<NCard title="基础表单">
<template #header-extra>
<NButton type="primary" @click="setFormValues">设置表单值</NButton>
</template>
<Form />
</NCard>
</Page>
</template>

View File

@ -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 模态框',

View File

@ -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
<script lang="ts" setup>
import { ApiComponent } from '@vben/common-ui';
import { Cascader } from 'ant-design-vue';
const treeData: Record<string, any> = [
{
label: '浙江',
value: 'zhejiang',
children: [
{
value: 'hangzhou',
label: '杭州',
children: [
{
value: 'xihu',
label: '西湖',
},
{
value: 'sudi',
label: '苏堤',
},
],
},
{
value: 'jiaxing',
label: '嘉兴',
children: [
{
value: 'wuzhen',
label: '乌镇',
},
{
value: 'meihuazhou',
label: '梅花洲',
},
],
},
{
value: 'zhoushan',
label: '舟山',
children: [
{
value: 'putuoshan',
label: '普陀山',
},
{
value: 'taohuadao',
label: '桃花岛',
},
],
},
],
},
{
label: '江苏',
value: 'jiangsu',
children: [
{
value: 'nanjing',
label: '南京',
children: [
{
value: 'zhonghuamen',
label: '中华门',
},
{
value: 'zijinshan',
label: '紫金山',
},
{
value: 'yuhuatai',
label: '雨花台',
},
],
},
],
},
];
/**
* 模拟请求接口
*/
function fetchApi(): Promise<Record<string, any>> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(treeData);
}, 1000);
});
}
</script>
<template>
<ApiComponent
:api="fetchApi"
:component="Cascader"
:immediate="false"
children-field="children"
loading-slot="suffixIcon"
visible-event="onDropdownVisibleChange"
/>
</template>
```
:::
### Props
| 属性名 | 描述 | 类型 | 默认值 |
| --- | --- | --- | --- |
| component | 欲包装的组件 | `Component` | - |
| numberToString | 是否将value从数字转为string | `boolean` | `false` |
| api | 获取数据的函数 | `(arg?: any) => Promise<OptionsItem[] \| Record<string, any>>` | - |
| params | 传递给api的参数 | `Record<string, any>` | - |
| 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<any, any>` | - |
| afterFetch | 在api请求之后的回调函数 | `AnyPromiseFunction<any, any>` | - |
| options | 直接传入选项数据也作为api返回空数据时的后备数据 | `OptionsItem[]` | - |
| visibleEvent | 触发重新请求数据的事件名 | `string` | - |
| loadingSlot | 组件的插槽名称,用来显示一个"加载中"的图标 | `string` | - |
```
```

View File

@ -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

View File

@ -306,6 +306,8 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
| actionWrapperClass | 表单操作区域class | `any` | - |
| handleReset | 表单重置回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
| handleSubmit | 表单提交回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
| handleValuesChange | 表单值变化回调 | `(values: Record<string, any>,) => void` | - |
| actionButtonsReverse | 调换操作按钮位置 | `boolean` | `false` |
| resetButtonOptions | 重置按钮组件参数 | `ActionButtonOptions` | - |
| submitButtonOptions | 提交按钮组件参数 | `ActionButtonOptions` | - |
| showDefaultActions | 是否显示默认操作按钮 | `boolean` | `true` |

View File

@ -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

View File

@ -0,0 +1,100 @@
<script lang="ts" setup>
import { ApiComponent } from '@vben/common-ui';
import { Cascader } from 'ant-design-vue';
const treeData: Record<string, any> = [
{
label: '浙江',
value: 'zhejiang',
children: [
{
value: 'hangzhou',
label: '杭州',
children: [
{
value: 'xihu',
label: '西湖',
},
{
value: 'sudi',
label: '苏堤',
},
],
},
{
value: 'jiaxing',
label: '嘉兴',
children: [
{
value: 'wuzhen',
label: '乌镇',
},
{
value: 'meihuazhou',
label: '梅花洲',
},
],
},
{
value: 'zhoushan',
label: '舟山',
children: [
{
value: 'putuoshan',
label: '普陀山',
},
{
value: 'taohuadao',
label: '桃花岛',
},
],
},
],
},
{
label: '江苏',
value: 'jiangsu',
children: [
{
value: 'nanjing',
label: '南京',
children: [
{
value: 'zhonghuamen',
label: '中华门',
},
{
value: 'zijinshan',
label: '紫金山',
},
{
value: 'yuhuatai',
label: '雨花台',
},
],
},
],
},
];
/**
* 模拟请求接口
*/
function fetchApi(): Promise<Record<string, any>> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(treeData);
}, 1000);
});
}
</script>
<template>
<ApiComponent
:api="fetchApi"
:component="Cascader"
:immediate="false"
children-field="children"
loading-slot="suffixIcon"
visible-event="onDropdownVisibleChange"
/>
</template>

View File

@ -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": {

View File

@ -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
*/

View File

@ -142,13 +142,29 @@ defineExpose({
"
:style="queryFormStyle"
>
<template v-if="rootProps.actionButtonsReverse">
<!-- 提交按钮前 -->
<slot name="submit-before"></slot>
<component
:is="COMPONENT_MAP.PrimaryButton"
v-if="submitButtonOptions.show"
class="ml-3"
type="button"
@click="handleSubmit"
v-bind="submitButtonOptions"
>
{{ submitButtonOptions.content }}
</component>
</template>
<!-- 重置按钮前 -->
<slot name="reset-before"></slot>
<component
:is="COMPONENT_MAP.DefaultButton"
v-if="resetButtonOptions.show"
class="mr-3"
class="ml-3"
type="button"
@click="handleReset"
v-bind="resetButtonOptions"
@ -156,18 +172,21 @@ defineExpose({
{{ resetButtonOptions.content }}
</component>
<!-- 提交按钮前 -->
<slot name="submit-before"></slot>
<template v-if="!rootProps.actionButtonsReverse">
<!-- 提交按钮前 -->
<slot name="submit-before"></slot>
<component
:is="COMPONENT_MAP.PrimaryButton"
v-if="submitButtonOptions.show"
type="button"
@click="handleSubmit"
v-bind="submitButtonOptions"
>
{{ submitButtonOptions.content }}
</component>
<component
:is="COMPONENT_MAP.PrimaryButton"
v-if="submitButtonOptions.show"
class="ml-3"
type="button"
@click="handleSubmit"
v-bind="submitButtonOptions"
>
{{ submitButtonOptions.content }}
</component>
</template>
<!-- 展开按钮前 -->
<slot name="expand-before"></slot>

View File

@ -307,6 +307,10 @@ export interface VbenFormProps<
FormRenderProps<T>,
'componentBindEventMap' | 'componentMap' | 'form'
> {
/**
*
*/
actionButtonsReverse?: boolean;
/**
* class
*/

View File

@ -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 },

View File

@ -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:"

View File

@ -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;
</script>
<template>
@ -553,6 +556,7 @@ function handleHeaderToggle() {
<!-- </div> -->
<LayoutContent
:id="idMainContent"
:content-compact="contentCompact"
:content-compact-width="contentCompactWidth"
:padding="contentPadding"

View File

@ -7,6 +7,11 @@ import type { Component, Ref } from 'vue';
export type DrawerPlacement = 'bottom' | 'left' | 'right' | 'top';
export interface DrawerProps {
/**
*
* @default false
*/
appendToMain?: boolean;
/**
*
*/
@ -59,12 +64,12 @@ export interface DrawerProps {
*
*/
headerClass?: ClassType;
/**
*
* @default false
*/
loading?: boolean;
/**
*
* @default true
@ -74,12 +79,12 @@ export interface DrawerProps {
*
*/
openAutoFocus?: boolean;
/**
*
* @default right
*/
placement?: DrawerPlacement;
/**
*
* @default true
@ -98,6 +103,10 @@ export interface DrawerProps {
*
*/
titleTooltip?: string;
/**
*
*/
zIndex?: number;
}
export interface DrawerState extends DrawerProps {

View File

@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { DrawerProps, ExtendedDrawerApi } from './drawer';
import { provide, ref, useId, watch } from 'vue';
import { computed, provide, ref, useId, watch } from 'vue';
import {
useIsMobile,
@ -23,6 +23,7 @@ import {
VbenLoading,
VisuallyHidden,
} from '@vben-core/shadcn-ui';
import { ELEMENT_ID_MAIN_CONTENT } from '@vben-core/shared/constants';
import { globalShareState } from '@vben-core/shared/global-state';
import { cn } from '@vben-core/shared/utils';
@ -31,7 +32,9 @@ interface Props extends DrawerProps {
}
const props = withDefaults(defineProps<Props>(), {
appendToMain: false,
drawerApi: undefined,
zIndex: 1000,
});
const components = globalShareState.getComponents();
@ -46,6 +49,7 @@ const { isMobile } = useIsMobile();
const state = props.drawerApi?.useStore?.();
const {
appendToMain,
cancelText,
class: drawerClass,
closable,
@ -67,6 +71,7 @@ const {
showConfirmButton,
title,
titleTooltip,
zIndex,
} = usePriorityValues(props, state);
watch(
@ -110,6 +115,10 @@ function handleFocusOutside(e: Event) {
e.preventDefault();
e.stopPropagation();
}
const getAppendTo = computed(() => {
return appendToMain.value ? `#${ELEMENT_ID_MAIN_CONTENT}` : undefined;
});
</script>
<template>
<Sheet
@ -118,6 +127,7 @@ function handleFocusOutside(e: Event) {
@update:open="() => drawerApi?.close()"
>
<SheetContent
:append-to="getAppendTo"
:class="
cn('flex w-[520px] flex-col', drawerClass, {
'!w-full': isMobile || placement === 'bottom' || placement === 'top',
@ -127,6 +137,7 @@ function handleFocusOutside(e: Event) {
:modal="modal"
:open="state?.isOpen"
:side="placement"
:z-index="zIndex"
@close-auto-focus="handleFocusOutside"
@escape-key-down="escapeKeyDown"
@focus-outside="handleFocusOutside"

View File

@ -3,6 +3,11 @@ import type { ModalApi } from './modal-api';
import type { Component, Ref } from 'vue';
export interface ModalProps {
/**
*
* @default false
*/
appendToMain?: boolean;
/**
*
* @default false
@ -12,7 +17,6 @@ export interface ModalProps {
*
*/
cancelText?: string;
/**
*
* @default false
@ -20,6 +24,7 @@ export interface ModalProps {
centered?: boolean;
class?: string;
/**
*
* @default true
@ -112,6 +117,10 @@ export interface ModalProps {
*
*/
titleTooltip?: string;
/**
*
*/
zIndex?: number;
}
export interface ModalState extends ModalProps {

View File

@ -22,6 +22,7 @@ import {
VbenLoading,
VisuallyHidden,
} from '@vben-core/shadcn-ui';
import { ELEMENT_ID_MAIN_CONTENT } from '@vben-core/shared/constants';
import { globalShareState } from '@vben-core/shared/global-state';
import { cn } from '@vben-core/shared/utils';
@ -32,6 +33,7 @@ interface Props extends ModalProps {
}
const props = withDefaults(defineProps<Props>(), {
appendToMain: false,
modalApi: undefined,
});
@ -52,6 +54,7 @@ const { isMobile } = useIsMobile();
const state = props.modalApi?.useStore?.();
const {
appendToMain,
bordered,
cancelText,
centered,
@ -78,6 +81,7 @@ const {
showConfirmButton,
title,
titleTooltip,
zIndex,
} = usePriorityValues(props, state);
const shouldFullscreen = computed(
@ -161,6 +165,9 @@ function handleFocusOutside(e: Event) {
e.preventDefault();
e.stopPropagation();
}
const getAppendTo = computed(() => {
return appendToMain.value ? `#${ELEMENT_ID_MAIN_CONTENT}` : undefined;
});
</script>
<template>
<Dialog
@ -170,6 +177,7 @@ function handleFocusOutside(e: Event) {
>
<DialogContent
ref="contentRef"
:append-to="getAppendTo"
:class="
cn(
'left-0 right-0 top-[10vh] mx-auto flex max-h-[80%] w-[520px] flex-col p-0 sm:rounded-[var(--radius)]',
@ -187,6 +195,7 @@ function handleFocusOutside(e: Event) {
:modal="modal"
:open="state?.isOpen"
:show-close="closable"
:z-index="zIndex"
close-class="top-3"
@close-auto-focus="handleFocusOutside"
@closed="() => modalApi?.onClosed()"

View File

@ -20,14 +20,16 @@ import DialogOverlay from './DialogOverlay.vue';
const props = withDefaults(
defineProps<
{
appendTo?: HTMLElement | string;
class?: ClassType;
closeClass?: ClassType;
modal?: boolean;
open?: boolean;
showClose?: boolean;
zIndex?: number;
} & DialogContentProps
>(),
{ showClose: true },
{ appendTo: 'body', showClose: true, zIndex: 1000 },
);
const emits = defineEmits<
{ close: []; closed: []; opened: [] } & DialogContentEmits
@ -45,6 +47,18 @@ const delegatedProps = computed(() => {
return delegated;
});
function isAppendToBody() {
return (
props.appendTo === 'body' ||
props.appendTo === document.body ||
!props.appendTo
);
}
const position = computed(() => {
return isAppendToBody() ? 'fixed' : 'absolute';
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const contentRef = ref<InstanceType<typeof DialogContent> | null>(null);
@ -64,17 +78,22 @@ defineExpose({
</script>
<template>
<DialogPortal>
<DialogPortal :to="appendTo">
<Transition name="fade">
<DialogOverlay v-if="open && modal" @click="() => emits('close')" />
<DialogOverlay
v-if="open && modal"
:style="{ zIndex, position }"
@click="() => emits('close')"
/>
</Transition>
<DialogContent
ref="contentRef"
:style="{ zIndex, position }"
@animationend="onAnimationEnd"
v-bind="forwarded"
:class="
cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%] fixed z-[1000] w-full p-6 shadow-lg outline-none sm:rounded-xl',
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%] w-full p-6 shadow-lg outline-none sm:rounded-xl',
props.class,
)
"

View File

@ -7,8 +7,5 @@ useScrollLock();
const id = inject('DISMISSABLE_MODAL_ID');
</script>
<template>
<div
:data-dismissable-modal="id"
class="bg-overlay fixed inset-0 z-[1000]"
></div>
<div :data-dismissable-modal="id" class="bg-overlay inset-0"></div>
</template>

View File

@ -14,7 +14,10 @@ import {
useForwardPropsEmits,
} from 'radix-vue';
const props = defineProps<{ class?: any } & DialogContentProps>();
const props = withDefaults(
defineProps<{ class?: any; zIndex?: number } & DialogContentProps>(),
{ zIndex: 1000 },
);
const emits = defineEmits<DialogContentEmits>();
const delegatedProps = computed(() => {
@ -29,7 +32,8 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
<template>
<DialogPortal>
<DialogOverlay
class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 border-border fixed inset-0 z-[1000] grid place-items-center overflow-y-auto border bg-black/80"
:style="{ zIndex }"
class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 border-border absolute inset-0 grid place-items-center overflow-y-auto border bg-black/80"
>
<DialogContent
:class="
@ -38,6 +42,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
props.class,
)
"
:style="{ zIndex }"
v-bind="forwarded"
@pointer-down-outside="
(event) => {

View File

@ -15,17 +15,22 @@ import { type SheetVariants, sheetVariants } from './sheet';
import SheetOverlay from './SheetOverlay.vue';
interface SheetContentProps extends DialogContentProps {
appendTo?: HTMLElement | string;
class?: any;
modal?: boolean;
open?: boolean;
side?: SheetVariants['side'];
zIndex?: number;
}
defineOptions({
inheritAttrs: false,
});
const props = defineProps<SheetContentProps>();
const props = withDefaults(defineProps<SheetContentProps>(), {
appendTo: 'body',
zIndex: 1000,
});
const emits = defineEmits<DialogContentEmits>();
@ -41,16 +46,29 @@ const delegatedProps = computed(() => {
return delegated;
});
function isAppendToBody() {
return (
props.appendTo === 'body' ||
props.appendTo === document.body ||
!props.appendTo
);
}
const position = computed(() => {
return isAppendToBody() ? 'fixed' : 'absolute';
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DialogPortal>
<DialogPortal :to="appendTo">
<Transition name="fade">
<SheetOverlay v-if="open && modal" />
<SheetOverlay v-if="open && modal" :style="{ zIndex, position }" />
</Transition>
<DialogContent
:class="cn(sheetVariants({ side }), 'z-[1000]', props.class)"
:class="cn(sheetVariants({ side }), props.class)"
:style="{ zIndex, position }"
v-bind="{ ...forwarded, ...$attrs }"
>
<slot></slot>

View File

@ -7,8 +7,5 @@ useScrollLock();
const id = inject('DISMISSABLE_DRAWER_ID');
</script>
<template>
<div
:data-dismissable-drawer="id"
class="bg-overlay fixed inset-0 z-[1000]"
></div>
<div :data-dismissable-drawer="id" class="bg-overlay inset-0"></div>
</template>

View File

@ -1,7 +1,7 @@
import { cva, type VariantProps } from 'class-variance-authority';
export const sheetVariants = cva(
'fixed z-[1000] bg-background shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500 border-border',
'bg-background shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500 border-border',
{
defaultVariants: {
side: 'right',
@ -12,7 +12,7 @@ export const sheetVariants = cva(
'inset-x-0 bottom-0 border-t border-border data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left ',
right:
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right',
'inset-y-0 right-0 w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right',
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
},
},

View File

@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { AnyPromiseFunction } from '@vben/types';
import { computed, ref, unref, useAttrs, type VNode, watch } from 'vue';
import { type Component, computed, ref, unref, useAttrs, watch } from 'vue';
import { LoaderCircle } from '@vben/icons';
import { get, isEqual, isFunction } from '@vben-core/shared/utils';
@ -10,37 +10,56 @@ import { objectOmit } from '@vueuse/core';
type OptionsItem = {
[name: string]: any;
children?: OptionsItem[];
disabled?: boolean;
label?: string;
value?: string;
};
interface Props {
//
component: VNode;
/** 组件 */
component: Component;
/** 是否将value从数字转为string */
numberToString?: boolean;
/** 获取options数据的函数 */
api?: (arg?: any) => Promise<OptionsItem[] | Record<string, any>>;
/** 传递给api的参数 */
params?: Record<string, any>;
/** 从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<any, any>;
/** 在api请求之后的回调函数 */
afterFetch?: AnyPromiseFunction<any, any>;
/** 直接传入选项数据也作为api返回空数据时的后备数据 */
options?: OptionsItem[];
//
/** 组件的插槽名称,用来显示一个"加载中"的图标 */
loadingSlot?: string;
//
/** 触发api请求的事件名 */
visibleEvent?: string;
modelField?: string;
/** 组件的v-model属性名默认为modelValue。部分组件可能为value */
modelPropName?: string;
}
defineOptions({ name: 'ApiSelect', inheritAttrs: false });
defineOptions({ name: 'ApiComponent', inheritAttrs: false });
const props = withDefaults(defineProps<Props>(), {
labelField: 'label',
valueField: 'value',
childrenField: '',
optionsPropName: 'options',
resultField: '',
visibleEvent: '',
numberToString: false,
@ -50,7 +69,7 @@ const props = withDefaults(defineProps<Props>(), {
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() {
<component
:is="component"
v-bind="bindProps"
:options="getOptions"
:placeholder="$attrs.placeholder"
>
<template v-for="item in Object.keys($slots)" #[item]="data">

View File

@ -0,0 +1 @@
export { default as ApiComponent } from './api-component.vue';

View File

@ -1 +0,0 @@
export { default as ApiSelect } from './api-select.vue';

View File

@ -1,4 +1,4 @@
export * from './api-select';
export * from './api-component';
export * from './captcha';
export * from './code-mirror';
export * from './ellipsis-text';

View File

@ -1,5 +1,12 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, ref, useTemplateRef } from 'vue';
import {
computed,
nextTick,
onMounted,
ref,
type StyleValue,
useTemplateRef,
} from 'vue';
import { CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT } from '@vben-core/shared/constants';
import { cn } from '@vben-core/shared/utils';
@ -29,13 +36,11 @@ const shouldAutoHeight = ref(false);
const headerRef = useTemplateRef<HTMLDivElement>('headerRef');
const footerRef = useTemplateRef<HTMLDivElement>('footerRef');
const contentStyle = computed(() => {
const contentStyle = computed<StyleValue>(() => {
if (autoContentHeight) {
return {
height: shouldAutoHeight.value
? `calc(var(${CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT}) - ${headerHeight.value}px)`
: '0',
// 'overflow-y': shouldAutoHeight.value?'auto':'unset',
height: `calc(var(${CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT}) - ${headerHeight.value}px)`,
overflowY: shouldAutoHeight.value ? 'auto' : 'unset',
};
}
return {};

View File

@ -16,6 +16,7 @@ import {
VxeInput,
VxeLoading,
VxeModal,
VxeNumberInput,
VxePager,
// VxeList,
// VxeModal,
@ -82,6 +83,7 @@ export function initVxeTable() {
// VxeUI.component(VxeList);
VxeUI.component(VxeLoading);
VxeUI.component(VxeModal);
VxeUI.component(VxeNumberInput);
// VxeUI.component(VxeOptgroup);
// VxeUI.component(VxeOption);
VxeUI.component(VxePager);

View File

@ -42,7 +42,7 @@ export interface VxeGridProps {
/**
* vxe-grid
*/
gridEvents?: DeepPartial<VxeGridListeners>;
gridEvents?: Partial<VxeGridListeners>;
/**
*
*/

View File

@ -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 {
@ -49,6 +49,7 @@ const withDefaultPlaceholder = <T extends Component>(
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType =
| 'ApiSelect'
| 'ApiTreeSelect'
| 'AutoComplete'
| 'Checkbox'
| 'CheckboxGroup'
@ -82,13 +83,31 @@ async function initComponentAdapter() {
ApiSelect: (props, { attrs, slots }) => {
return h(
ApiSelect,
ApiComponent,
{
placeholder: $t('ui.placeholder.select'),
...props,
...attrs,
component: Select,
loadingSlot: 'suffixIcon',
modelField: 'value',
modelPropName: 'value',
visibleEvent: 'onVisibleChange',
},
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,

View File

@ -0,0 +1,21 @@
<script lang="ts" setup>
import { useVbenDrawer } from '@vben/common-ui';
import { message } from 'ant-design-vue';
const [Drawer, drawerApi] = useVbenDrawer({
onCancel() {
drawerApi.close();
},
onConfirm() {
message.info('onConfirm');
// drawerApi.close();
},
});
</script>
<template>
<Drawer append-to-main title="基础抽屉示例" title-tooltip="标题提示内容">
<template #extra> extra </template>
本抽屉指定在内容区域打开
</Drawer>
</template>

View File

@ -8,6 +8,7 @@ import AutoHeightDemo from './auto-height-demo.vue';
import BaseDemo from './base-demo.vue';
import DynamicDemo from './dynamic-demo.vue';
import FormDrawerDemo from './form-drawer-demo.vue';
import inContentDemo from './in-content-demo.vue';
import SharedDataDemo from './shared-data-demo.vue';
const [BaseDrawer, baseDrawerApi] = useVbenDrawer({
@ -16,6 +17,12 @@ const [BaseDrawer, baseDrawerApi] = useVbenDrawer({
// placement: 'left',
});
const [InContentDrawer, inContentDrawerApi] = useVbenDrawer({
//
connectedComponent: inContentDemo,
// placement: 'left',
});
const [AutoHeightDrawer, autoHeightDrawerApi] = useVbenDrawer({
connectedComponent: AutoHeightDemo,
});
@ -37,6 +44,23 @@ function openBaseDrawer(placement: DrawerPlacement = 'right') {
baseDrawerApi.open();
}
function openInContentDrawer(placement: DrawerPlacement = 'right') {
inContentDrawerApi.setState({ class: '', placement });
if (placement === 'top') {
// 200200
inContentDrawerApi.setState({ zIndex: 199 });
} else {
inContentDrawerApi.setState({ zIndex: undefined });
}
inContentDrawerApi.open();
}
function openMaxContentDrawer() {
// 便使Drawer
inContentDrawerApi.setState({ class: 'w-full', placement: 'right' });
inContentDrawerApi.open();
}
function openAutoHeightDrawer() {
autoHeightDrawerApi.open();
}
@ -69,6 +93,7 @@ function openFormDrawer() {
<template>
<Page
auto-content-height
description="抽屉组件通常用于在当前页面上显示一个覆盖层,用以展示重要信息或提供用户交互界面。"
title="抽屉组件示例"
>
@ -76,6 +101,7 @@ function openFormDrawer() {
<DocButton path="/components/common-ui/vben-drawer" />
</template>
<BaseDrawer />
<InContentDrawer />
<AutoHeightDrawer />
<DynamicDrawer />
<SharedDataDrawer />
@ -83,18 +109,55 @@ function openFormDrawer() {
<Card class="mb-4" title="基本使用">
<p class="mb-3">一个基础的抽屉示例</p>
<Button type="primary" @click="openBaseDrawer('right')">右侧打开</Button>
<Button class="ml-2" type="primary" @click="openBaseDrawer('bottom')">
<Button class="mb-2" type="primary" @click="openBaseDrawer('right')">
右侧打开
</Button>
<Button
class="mb-2 ml-2"
type="primary"
@click="openBaseDrawer('bottom')"
>
底部打开
</Button>
<Button class="ml-2" type="primary" @click="openBaseDrawer('left')">
<Button class="mb-2 ml-2" type="primary" @click="openBaseDrawer('left')">
左侧打开
</Button>
<Button class="ml-2" type="primary" @click="openBaseDrawer('top')">
<Button class="mb-2 ml-2" type="primary" @click="openBaseDrawer('top')">
顶部打开
</Button>
</Card>
<Card class="mb-4" title="在内容区域打开">
<p class="mb-3">指定抽屉在内容区域打开不会覆盖顶部和左侧菜单等区域</p>
<Button class="mb-2" type="primary" @click="openInContentDrawer('right')">
右侧打开
</Button>
<Button
class="mb-2 ml-2"
type="primary"
@click="openInContentDrawer('bottom')"
>
底部打开
</Button>
<Button
class="mb-2 ml-2"
type="primary"
@click="openInContentDrawer('left')"
>
左侧打开
</Button>
<Button
class="mb-2 ml-2"
type="primary"
@click="openInContentDrawer('top')"
>
顶部打开
</Button>
<Button class="mb-2 ml-2" type="primary" @click="openMaxContentDrawer">
内容区域全屏打开
</Button>
</Card>
<Card class="mb-4" title="内容高度自适应滚动">
<p class="mb-3">可根据内容自动计算滚动高度</p>
<Button type="primary" @click="openAutoHeightDrawer">打开抽屉</Button>

View File

@ -1,11 +1,17 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { Page } from '@vben/common-ui';
import { Button, Card, message, Space } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
const isReverseActionButtons = ref(false);
const [BaseForm, formApi] = useVbenForm({
//
actionButtonsReverse: isReverseActionButtons.value,
//
commonConfig: {
//
@ -83,6 +89,7 @@ function handleClick(
| 'labelWidth'
| 'resetDisabled'
| 'resetLabelWidth'
| 'reverseActionButtons'
| 'showAction'
| 'showResetButton'
| 'showSubmitButton'
@ -158,6 +165,11 @@ function handleClick(
});
break;
}
case 'reverseActionButtons': {
isReverseActionButtons.value = !isReverseActionButtons.value;
formApi.setState({ actionButtonsReverse: isReverseActionButtons.value });
break;
}
case 'showAction': {
formApi.setState({ showDefaultActions: true });
break;
@ -177,6 +189,7 @@ function handleClick(
});
break;
}
case 'updateResetButton': {
formApi.setState({
resetButtonOptions: { disabled: true },
@ -226,6 +239,9 @@ function handleClick(
<Button @click="handleClick('resetLabelWidth')">还原labelWidth</Button>
<Button @click="handleClick('disabled')">禁用表单</Button>
<Button @click="handleClick('resetDisabled')">解除禁用</Button>
<Button @click="handleClick('reverseActionButtons')">
翻转操作按钮位置
</Button>
<Button @click="handleClick('hiddenAction')">隐藏操作按钮</Button>
<Button @click="handleClick('showAction')">显示操作按钮</Button>
<Button @click="handleClick('hiddenResetButton')">隐藏重置按钮</Button>

View File

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

View File

@ -0,0 +1,25 @@
<script lang="ts" setup>
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
const [Modal, modalApi] = useVbenModal({
onCancel() {
modalApi.close();
},
onConfirm() {
message.info('onConfirm');
// modalApi.close();
},
});
</script>
<template>
<Modal
append-to-main
class="w-[600px]"
title="基础弹窗示例"
title-tooltip="标题提示内容"
>
此弹窗指定在内容区域打开
</Modal>
</template>

View File

@ -9,6 +9,7 @@ import BaseDemo from './base-demo.vue';
import DragDemo from './drag-demo.vue';
import DynamicDemo from './dynamic-demo.vue';
import FormModalDemo from './form-modal-demo.vue';
import InContentModalDemo from './in-content-demo.vue';
import SharedDataDemo from './shared-data-demo.vue';
const [BaseModal, baseModalApi] = useVbenModal({
@ -16,6 +17,11 @@ const [BaseModal, baseModalApi] = useVbenModal({
connectedComponent: BaseDemo,
});
const [InContentModal, inContentModalApi] = useVbenModal({
//
connectedComponent: InContentModalDemo,
});
const [AutoHeightModal, autoHeightModalApi] = useVbenModal({
connectedComponent: AutoHeightDemo,
});
@ -40,6 +46,10 @@ function openBaseModal() {
baseModalApi.open();
}
function openInContentModal() {
inContentModalApi.open();
}
function openAutoHeightModal() {
autoHeightModalApi.open();
}
@ -76,6 +86,7 @@ function openFormModal() {
<template>
<Page
auto-content-height
description="弹窗组件常用于在不离开当前页面的情况下显示额外的信息、表单或操作提示更多api请查看组件文档。"
title="弹窗组件示例"
>
@ -83,6 +94,7 @@ function openFormModal() {
<DocButton path="/components/common-ui/vben-modal" />
</template>
<BaseModal />
<InContentModal />
<AutoHeightModal />
<DragModal />
<DynamicModal />
@ -93,6 +105,11 @@ function openFormModal() {
<Button type="primary" @click="openBaseModal">打开弹窗</Button>
</Card>
<Card class="mb-4" title="指定容器">
<p class="mb-3">在内容区域打开弹窗的示例</p>
<Button type="primary" @click="openInContentModal">打开弹窗</Button>
</Card>
<Card class="mb-4" title="内容高度自适应">
<p class="mb-3">可根据内容并自动调整高度</p>
<Button type="primary" @click="openAutoHeightModal">打开弹窗</Button>

View File

@ -13,7 +13,7 @@ packages:
- docs
- playground
catalog:
'@ast-grep/napi': ^0.31.0
'@ast-grep/napi': ^0.31.1
'@changesets/changelog-github': ^0.5.0
'@changesets/cli': ^2.27.10
'@changesets/git': ^3.0.2
@ -23,20 +23,20 @@ catalog:
'@ctrl/tinycolor': ^4.1.0
'@eslint/js': ^9.16.0
'@faker-js/faker': ^9.3.0
'@iconify/json': ^2.2.279
'@iconify/tailwind': ^1.1.3
'@iconify/vue': ^4.1.2
'@iconify/json': ^2.2.281
'@iconify/tailwind': ^1.2.0
'@iconify/vue': ^4.2.0
'@intlify/core-base': ^10.0.5
'@intlify/unplugin-vue-i18n': ^6.0.0
'@intlify/unplugin-vue-i18n': ^6.0.1
'@jspm/generator': ^2.4.1
'@manypkg/get-packages': ^2.2.2
'@nolebase/vitepress-plugin-git-changelog': ^2.11.1
'@playwright/test': ^1.49.0
'@playwright/test': ^1.49.1
'@pnpm/workspace.read-manifest': ^1000.0.0
'@stylistic/stylelint-plugin': ^3.1.1
'@tailwindcss/nesting': 0.0.0-insiders.565cd3e
'@tailwindcss/typography': ^0.5.15
'@tanstack/vue-query': ^5.62.2
'@tanstack/vue-query': ^5.62.7
'@tanstack/vue-store': ^0.6.0
'@types/archiver': ^6.0.3
'@types/eslint': ^9.6.1
@ -50,8 +50,8 @@ catalog:
'@types/postcss-import': ^14.0.3
'@types/qrcode': ^1.5.5
'@types/sortablejs': ^1.15.8
'@typescript-eslint/eslint-plugin': ^8.17.0
'@typescript-eslint/parser': ^8.17.0
'@typescript-eslint/eslint-plugin': ^8.18.0
'@typescript-eslint/parser': ^8.18.0
'@vee-validate/zod': ^4.14.7
'@vite-pwa/vitepress': ^0.5.3
'@vitejs/plugin-vue': ^5.2.1
@ -90,9 +90,9 @@ catalog:
eslint-plugin-command: ^0.2.6
eslint-plugin-eslint-comments: ^3.2.0
eslint-plugin-import-x: ^4.5.0
eslint-plugin-jsdoc: ^50.6.0
eslint-plugin-jsdoc: ^50.6.1
eslint-plugin-jsonc: ^2.18.2
eslint-plugin-n: ^17.14.0
eslint-plugin-n: ^17.15.0
eslint-plugin-no-only-tests: ^3.3.0
eslint-plugin-perfectionist: ^3.9.1
eslint-plugin-prettier: ^5.2.1
@ -101,7 +101,7 @@ catalog:
eslint-plugin-unused-imports: ^4.1.4
eslint-plugin-vitest: ^0.5.4
eslint-plugin-vue: ^9.32.0
execa: ^9.5.1
execa: ^9.5.2
find-up: ^7.0.0
get-port: ^7.1.0
globals: ^15.13.0
@ -112,7 +112,7 @@ catalog:
is-ci: ^3.0.1
jsonc-eslint-parser: ^2.4.0
jsonwebtoken: ^9.0.2
lint-staged: ^15.2.10
lint-staged: ^15.2.11
lodash.clonedeep: ^4.5.0
lodash.get: ^4.4.2
lodash.isequal: ^4.5.0
@ -125,7 +125,7 @@ catalog:
pinia: 2.2.2
pinia-plugin-persistedstate: ^4.1.3
pkg-types: ^1.2.1
playwright: ^1.49.0
playwright: ^1.49.1
postcss: ^8.4.49
postcss-antd-fixes: ^0.2.0
postcss-html: ^1.7.0
@ -136,10 +136,10 @@ catalog:
prettier-plugin-tailwindcss: ^0.6.9
publint: ^0.2.12
qrcode: ^1.5.4
radix-vue: ^1.9.10
radix-vue: ^1.9.11
resolve.exports: ^2.0.3
rimraf: ^6.0.1
rollup: ^4.28.0
rollup: ^4.28.1
rollup-plugin-visualizer: ^5.12.0
sass: 1.80.6
sortablejs: ^1.15.6
@ -161,7 +161,7 @@ catalog:
unbuild: ^3.0.0-rc.11
unplugin-element-plus: ^0.8.0
vee-validate: ^4.14.7
vite: ^6.0.2
vite: ^6.0.3
vite-plugin-compression: ^0.5.1
vite-plugin-dts: 4.2.1
vite-plugin-html: ^3.2.2
@ -179,5 +179,5 @@ catalog:
vxe-pc-ui: ^4.3.4
vxe-table: 4.9.5
watermark-js-plus: ^1.5.7
zod: ^3.23.8
zod: ^3.24.1
zod-defaults: ^0.1.3