pull/58/MERGE
xingyu4j 2024-12-11 16:45:50 +08:00
commit eb0d43e26c
54 changed files with 1832 additions and 446 deletions

View File

@ -10,7 +10,7 @@ import type { CustomComponentType } from '#/components/form/types';
import type { Component, SetupContext } from 'vue'; import type { Component, SetupContext } from 'vue';
import { h } 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 { $t } from '@vben/locales';
import { import {
@ -53,6 +53,7 @@ const withDefaultPlaceholder = <T extends Component>(
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明 // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType = export type ComponentType =
| 'ApiSelect' | 'ApiSelect'
| 'ApiTreeSelect'
| 'AutoComplete' | 'AutoComplete'
| 'Checkbox' | 'Checkbox'
| 'CheckboxGroup' | 'CheckboxGroup'
@ -86,14 +87,32 @@ async function initComponentAdapter() {
// import('xxx').then((res) => res.Button), // import('xxx').then((res) => res.Button),
ApiSelect: (props, { attrs, slots }) => { ApiSelect: (props, { attrs, slots }) => {
return h( return h(
ApiSelect, ApiComponent,
{ {
placeholder: $t('ui.placeholder.select'),
...props, ...props,
...attrs, ...attrs,
component: Select, component: Select,
loadingSlot: 'suffixIcon', loadingSlot: 'suffixIcon',
visibleEvent: 'onDropdownVisibleChange', 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, slots,
); );

View File

@ -4,24 +4,27 @@
*/ */
import type { BaseFormComponentType } from '@vben/common-ui'; import type { BaseFormComponentType } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import type { Component, SetupContext } from 'vue'; import type { Component, SetupContext } from 'vue';
import { h } 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 { $t } from '@vben/locales';
import { import {
ElButton, ElButton,
ElCheckbox, ElCheckbox,
ElCheckboxButton,
ElCheckboxGroup, ElCheckboxGroup,
ElDatePicker, ElDatePicker,
ElDivider, ElDivider,
ElInput, ElInput,
ElInputNumber, ElInputNumber,
ElNotification, ElNotification,
ElRadio,
ElRadioButton,
ElRadioGroup, ElRadioGroup,
ElSelect,
ElSelectV2, ElSelectV2,
ElSpace, ElSpace,
ElSwitch, ElSwitch,
@ -43,6 +46,7 @@ const withDefaultPlaceholder = <T extends Component>(
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明 // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType = export type ComponentType =
| 'ApiSelect' | 'ApiSelect'
| 'ApiTreeSelect'
| 'Checkbox' | 'Checkbox'
| 'CheckboxGroup' | 'CheckboxGroup'
| 'DatePicker' | 'DatePicker'
@ -66,19 +70,55 @@ async function initComponentAdapter() {
// import('xxx').then((res) => res.Button), // import('xxx').then((res) => res.Button),
ApiSelect: (props, { attrs, slots }) => { ApiSelect: (props, { attrs, slots }) => {
return h( return h(
ApiSelect, ApiComponent,
{ {
placeholder: $t('ui.placeholder.select'),
...props, ...props,
...attrs, ...attrs,
component: ElSelectV2, component: ElSelectV2,
loadingSlot: 'loading', 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, slots,
); );
}, },
Checkbox: ElCheckbox, 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 }) => { DefaultButton: (props, { attrs, slots }) => {
return h(ElButton, { ...props, attrs, type: 'info' }, slots); return h(ElButton, { ...props, attrs, type: 'info' }, slots);
@ -103,12 +143,72 @@ async function initComponentAdapter() {
}, },
Input: withDefaultPlaceholder(ElInput, 'input'), Input: withDefaultPlaceholder(ElInput, 'input'),
InputNumber: withDefaultPlaceholder(ElInputNumber, 'input'), InputNumber: withDefaultPlaceholder(ElInputNumber, 'input'),
RadioGroup: ElRadioGroup, RadioGroup: (props, { attrs, slots }) => {
Select: withDefaultPlaceholder(ElSelect, 'select'), 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, Space: ElSpace,
Switch: ElSwitch, Switch: ElSwitch,
TimePicker: ElTimePicker, TimePicker: (props, { attrs, slots }) => {
DatePicker: ElDatePicker, const { name, id, isRange } = props;
const extraProps: Recordable<any> = {};
if (isRange) {
if (name && !Array.isArray(name)) {
extraProps.name = [name, `${name}_end`];
}
if (id && !Array.isArray(id)) {
extraProps.id = [id, `${id}_end`];
}
}
return h(
ElTimePicker,
{
...props,
...attrs,
...extraProps,
},
slots,
);
},
DatePicker: (props, { attrs, slots }) => {
const { name, id, type } = props;
const extraProps: Recordable<any> = {};
if (type && type.includes('range')) {
if (name && !Array.isArray(name)) {
extraProps.name = [name, `${name}_end`];
}
if (id && !Array.isArray(id)) {
extraProps.id = [id, `${id}_end`];
}
}
return h(
ElDatePicker,
{
...props,
...attrs,
...extraProps,
},
slots,
);
},
TreeSelect: withDefaultPlaceholder(ElTreeSelect, 'select'), TreeSelect: withDefaultPlaceholder(ElTreeSelect, 'select'),
Upload: ElUpload, Upload: ElUpload,
}; };

View File

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

View File

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

View File

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

View File

@ -23,6 +23,14 @@ const routes: RouteRecordRaw[] = [
path: '/demos/element', path: '/demos/element',
component: () => import('#/views/demos/element/index.vue'), 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 type { Component, SetupContext } from 'vue';
import { h } 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 { $t } from '@vben/locales';
import { import {
@ -19,6 +19,8 @@ import {
NDivider, NDivider,
NInput, NInput,
NInputNumber, NInputNumber,
NRadio,
NRadioButton,
NRadioGroup, NRadioGroup,
NSelect, NSelect,
NSpace, NSpace,
@ -43,6 +45,7 @@ const withDefaultPlaceholder = <T extends Component>(
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明 // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType = export type ComponentType =
| 'ApiSelect' | 'ApiSelect'
| 'ApiTreeSelect'
| 'Checkbox' | 'Checkbox'
| 'CheckboxGroup' | 'CheckboxGroup'
| 'DatePicker' | 'DatePicker'
@ -67,18 +70,52 @@ async function initComponentAdapter() {
ApiSelect: (props, { attrs, slots }) => { ApiSelect: (props, { attrs, slots }) => {
return h( return h(
ApiSelect, ApiComponent,
{ {
placeholder: $t('ui.placeholder.select'),
...props, ...props,
...attrs, ...attrs,
component: NSelect, 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, slots,
); );
}, },
Checkbox: NCheckbox, 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, DatePicker: NDatePicker,
// 自定义默认按钮 // 自定义默认按钮
DefaultButton: (props, { attrs, slots }) => { DefaultButton: (props, { attrs, slots }) => {
@ -98,7 +135,28 @@ async function initComponentAdapter() {
}, },
Input: withDefaultPlaceholder(NInput, 'input'), Input: withDefaultPlaceholder(NInput, 'input'),
InputNumber: withDefaultPlaceholder(NInputNumber, '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'), Select: withDefaultPlaceholder(NSelect, 'select'),
Space: NSpace, Space: NSpace,
Switch: NSwitch, Switch: NSwitch,

View File

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

View File

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

View File

@ -31,6 +31,14 @@ const routes: RouteRecordRaw[] = [
path: '/demos/table', path: '/demos/table',
component: () => import('#/views/demos/table/index.vue'), 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, collapsed: false,
text: '通用组件', text: '通用组件',
items: [ items: [
{
link: 'common-ui/vben-api-component',
text: 'ApiComponent Api组件包装器',
},
{ {
link: 'common-ui/vben-modal', link: 'common-ui/vben-modal',
text: '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` | - | | title | 标题 | `string\|slot` | - |
| titleTooltip | 标题提示信息 | `string\|slot` | - | | titleTooltip | 标题提示信息 | `string\|slot` | - |
| description | 描述信息 | `string\|slot` | - | | description | 描述信息 | `string\|slot` | - |
@ -95,6 +96,13 @@ const [Drawer, drawerApi] = useVbenDrawer({
| contentClass | modal内容区域的class | `string` | - | | contentClass | modal内容区域的class | `string` | - |
| footerClass | modal底部区域的class | `string` | - | | footerClass | modal底部区域的class | `string` | - |
| headerClass | modal顶部区域的class | `string` | - | | headerClass | modal顶部区域的class | `string` | - |
| zIndex | 抽屉的ZIndex层级 | `number` | `1000` |
::: info appendToMain
`appendToMain`可以指定将抽屉挂载到内容区域打开抽屉时内容区域以外的部分标签栏、导航菜单等等不会被遮挡。默认情况下抽屉会挂载到body上。但是挂载到内容区域时作为页面根容器的`Page`组件,需要设置`auto-content-height`属性,以便抽屉能够正确计算高度。
:::
### Event ### Event

View File

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

View File

@ -80,6 +80,7 @@ const [Modal, modalApi] = useVbenModal({
| 属性名 | 描述 | 类型 | 默认值 | | 属性名 | 描述 | 类型 | 默认值 |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| appendToMain | 是否挂载到内容区域默认挂载到body | `boolean` | `false` |
| title | 标题 | `string\|slot` | - | | title | 标题 | `string\|slot` | - |
| titleTooltip | 标题提示信息 | `string\|slot` | - | | titleTooltip | 标题提示信息 | `string\|slot` | - |
| description | 描述信息 | `string\|slot` | - | | description | 描述信息 | `string\|slot` | - |
@ -106,6 +107,13 @@ const [Modal, modalApi] = useVbenModal({
| footerClass | modal底部区域的class | `string` | - | | footerClass | modal底部区域的class | `string` | - |
| headerClass | modal顶部区域的class | `string` | - | | headerClass | modal顶部区域的class | `string` | - |
| bordered | 是否显示border | `boolean` | `false` | | bordered | 是否显示border | `boolean` | `false` |
| zIndex | 弹窗的ZIndex层级 | `number` | `1000` |
::: info appendToMain
`appendToMain`可以指定将弹窗挂载到内容区域打开这种弹窗时内容区域以外的部分标签栏、导航菜单等等不会被遮挡。默认情况下弹窗会挂载到body上。但是挂载到内容区域时作为页面根容器的`Page`组件,需要设置`auto-content-height`属性,以便弹窗能够正确计算高度。
:::
### Event ### Event

View File

@ -18,15 +18,14 @@ outline: deep
### Props ### Props
| 属性名 | 描述 | 类型 | 默认值 | | 属性名 | 描述 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| title | 页面标题 | `string\|slot` | - | | title | 页面标题 | `string\|slot` | - | - |
| description | 页面描述(标题下的内容) | `string\|slot` | - | | description | 页面描述(标题下的内容) | `string\|slot` | - | - |
| contentClass | 内容区域的class | `string` | - | | contentClass | 内容区域的class | `string` | - | - |
| headerClass | 头部区域的class | `string` | - | | headerClass | 头部区域的class | `string` | - | - |
| footerClass | 底部区域的class | `string` | - | | footerClass | 底部区域的class | `string` | - | - |
| autoContentHeight | 自动调整内容区域的高度 | `boolean` | `false` | | autoContentHeight | 自动调整内容区域的高度 | `boolean` | `false` | - |
| fixedHeader | 固定头部在页面内容区域顶部,在滚动时保持可见 | `boolean` | `false` |
::: tip 注意 ::: tip 注意

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", "node": ">=20.10.0",
"pnpm": ">=9.12.0" "pnpm": ">=9.12.0"
}, },
"packageManager": "pnpm@9.14.4", "packageManager": "pnpm@9.15.0",
"pnpm": { "pnpm": {
"peerDependencyRules": { "peerDependencyRules": {
"allowedVersions": { "allowedVersions": {

View File

@ -7,6 +7,9 @@ export const CSS_VARIABLE_LAYOUT_HEADER_HEIGHT = `--vben-header-height`;
/** layout footer 组件的高度 */ /** layout footer 组件的高度 */
export const CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT = `--vben-footer-height`; export const CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT = `--vben-footer-height`;
/** 内容区域的组件ID */
export const ELEMENT_ID_MAIN_CONTENT = `__vben_main_content`;
/** /**
* @zh_CN * @zh_CN
*/ */

View File

@ -16,3 +16,11 @@ export function formatDate(time: number | string, format = 'YYYY-MM-DD') {
export function formatDateTime(time: number | string) { export function formatDateTime(time: number | string) {
return formatDate(time, 'YYYY-MM-DD HH:mm:ss'); return formatDate(time, 'YYYY-MM-DD HH:mm:ss');
} }
export function isDate(value: any): value is Date {
return value instanceof Date;
}
export function isDayjsObject(value: any): value is dayjs.Dayjs {
return dayjs.isDayjs(value);
}

View File

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

View File

@ -14,6 +14,8 @@ import { Store } from '@vben-core/shared/store';
import { import {
bindMethods, bindMethods,
createMerge, createMerge,
isDate,
isDayjsObject,
isFunction, isFunction,
isObject, isObject,
mergeWithArrayOverride, mergeWithArrayOverride,
@ -252,10 +254,19 @@ export class FormApi {
return; return;
} }
/**
* object
* antddayjs
* element-plusDate
*
*/
const fieldMergeFn = createMerge((obj, key, value) => { const fieldMergeFn = createMerge((obj, key, value) => {
if (key in obj) { if (key in obj) {
obj[key] = obj[key] =
!Array.isArray(obj[key]) && isObject(obj[key]) !Array.isArray(obj[key]) &&
isObject(obj[key]) &&
!isDayjsObject(obj[key]) &&
!isDate(obj[key])
? fieldMergeFn(obj[key], value) ? fieldMergeFn(obj[key], value)
: value; : value;
} }

View File

@ -307,11 +307,7 @@ function autofocus() {
> >
{{ label }} {{ label }}
</FormLabel> </FormLabel>
<div <div :class="cn('relative flex w-full items-center', wrapperClass)">
:class="
cn('relative flex w-full items-center overflow-hidden', wrapperClass)
"
>
<FormControl :class="cn(controlClass)"> <FormControl :class="cn(controlClass)">
<slot <slot
v-bind="{ v-bind="{

View File

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

View File

@ -6,7 +6,7 @@ import type { ExtendedFormApi, VbenFormProps } from './types';
import { useForwardPriorityValues } from '@vben-core/composables'; import { useForwardPriorityValues } from '@vben-core/composables';
// import { isFunction } from '@vben-core/shared/utils'; // import { isFunction } from '@vben-core/shared/utils';
import { useTemplateRef, watch } from 'vue'; import { toRaw, useTemplateRef, watch } from 'vue';
import { useDebounceFn } from '@vueuse/core'; import { useDebounceFn } from '@vueuse/core';
@ -62,6 +62,7 @@ function handleKeyDownEnter(event: KeyboardEvent) {
watch( watch(
() => form.values, () => form.values,
useDebounceFn(() => { useDebounceFn(() => {
forward.value.handleValuesChange?.(toRaw(form.values));
state.value.submitOnChange && props.formApi?.submitForm(); state.value.submitOnChange && props.formApi?.submitForm();
}, 300), }, 300),
{ deep: true }, { deep: true },

View File

@ -40,6 +40,7 @@
"@vben-core/composables": "workspace:*", "@vben-core/composables": "workspace:*",
"@vben-core/icons": "workspace:*", "@vben-core/icons": "workspace:*",
"@vben-core/shadcn-ui": "workspace:*", "@vben-core/shadcn-ui": "workspace:*",
"@vben-core/shared": "workspace:*",
"@vben-core/typings": "workspace:*", "@vben-core/typings": "workspace:*",
"@vueuse/core": "catalog:", "@vueuse/core": "catalog:",
"vue": "catalog:" "vue": "catalog:"

View File

@ -11,6 +11,7 @@ import {
} from '@vben-core/composables'; } from '@vben-core/composables';
import { Menu } from '@vben-core/icons'; import { Menu } from '@vben-core/icons';
import { VbenIconButton } from '@vben-core/shadcn-ui'; import { VbenIconButton } from '@vben-core/shadcn-ui';
import { ELEMENT_ID_MAIN_CONTENT } from '@vben-core/shared/constants';
import { useMouse, useScroll, useThrottleFn } from '@vueuse/core'; import { useMouse, useScroll, useThrottleFn } from '@vueuse/core';
@ -457,6 +458,8 @@ function handleHeaderToggle() {
emit('toggleSidebar'); emit('toggleSidebar');
} }
} }
const idMainContent = ELEMENT_ID_MAIN_CONTENT;
</script> </script>
<template> <template>
@ -503,7 +506,7 @@ function handleHeaderToggle() {
<div <div
ref="contentRef" ref="contentRef"
class="flex flex-1 flex-col transition-all duration-300 ease-in" class="flex flex-1 flex-col overflow-hidden transition-all duration-300 ease-in"
> >
<div <div
:class="[ :class="[
@ -553,6 +556,7 @@ function handleHeaderToggle() {
<!-- </div> --> <!-- </div> -->
<LayoutContent <LayoutContent
:id="idMainContent"
:content-compact="contentCompact" :content-compact="contentCompact"
:content-compact-width="contentCompactWidth" :content-compact-width="contentCompactWidth"
:padding="contentPadding" :padding="contentPadding"

View File

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

View File

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

View File

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

View File

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

View File

@ -20,14 +20,16 @@ import DialogOverlay from './DialogOverlay.vue';
const props = withDefaults( const props = withDefaults(
defineProps< defineProps<
{ {
appendTo?: HTMLElement | string;
class?: ClassType; class?: ClassType;
closeClass?: ClassType; closeClass?: ClassType;
modal?: boolean; modal?: boolean;
open?: boolean; open?: boolean;
showClose?: boolean; showClose?: boolean;
zIndex?: number;
} & DialogContentProps } & DialogContentProps
>(), >(),
{ showClose: true }, { appendTo: 'body', showClose: true, zIndex: 1000 },
); );
const emits = defineEmits< const emits = defineEmits<
{ close: []; closed: []; opened: [] } & DialogContentEmits { close: []; closed: []; opened: [] } & DialogContentEmits
@ -45,6 +47,18 @@ const delegatedProps = computed(() => {
return delegated; 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 forwarded = useForwardPropsEmits(delegatedProps, emits);
const contentRef = ref<InstanceType<typeof DialogContent> | null>(null); const contentRef = ref<InstanceType<typeof DialogContent> | null>(null);
@ -64,17 +78,22 @@ defineExpose({
</script> </script>
<template> <template>
<DialogPortal> <DialogPortal :to="appendTo">
<Transition name="fade"> <Transition name="fade">
<DialogOverlay v-if="open && modal" @click="() => emits('close')" /> <DialogOverlay
v-if="open && modal"
:style="{ zIndex, position }"
@click="() => emits('close')"
/>
</Transition> </Transition>
<DialogContent <DialogContent
ref="contentRef" ref="contentRef"
:style="{ zIndex, position }"
@animationend="onAnimationEnd" @animationend="onAnimationEnd"
v-bind="forwarded" v-bind="forwarded"
:class=" :class="
cn( 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, props.class,
) )
" "

View File

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

View File

@ -14,7 +14,10 @@ import {
useForwardPropsEmits, useForwardPropsEmits,
} from 'radix-vue'; } 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 emits = defineEmits<DialogContentEmits>();
const delegatedProps = computed(() => { const delegatedProps = computed(() => {
@ -29,7 +32,8 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
<template> <template>
<DialogPortal> <DialogPortal>
<DialogOverlay <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 <DialogContent
:class=" :class="
@ -38,6 +42,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
props.class, props.class,
) )
" "
:style="{ zIndex }"
v-bind="forwarded" v-bind="forwarded"
@pointer-down-outside=" @pointer-down-outside="
(event) => { (event) => {

View File

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

View File

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

View File

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

View File

@ -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 './captcha';
export * from './ellipsis-text'; export * from './ellipsis-text';
export * from './icon-picker'; export * from './icon-picker';

View File

@ -8,8 +8,7 @@ import {
useTemplateRef, useTemplateRef,
} from 'vue'; } from 'vue';
import { useLayoutFooterStyle } from '@vben/hooks'; import { CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT } from '@vben-core/shared/constants';
import { preferences } from '@vben-core/preferences';
import { cn } from '@vben-core/shared/utils'; import { cn } from '@vben-core/shared/utils';
interface Props { interface Props {
@ -20,8 +19,6 @@ interface Props {
* 根据content可见高度自适应 * 根据content可见高度自适应
*/ */
autoContentHeight?: boolean; autoContentHeight?: boolean;
/** 头部固定 */
fixedHeader?: boolean;
headerClass?: string; headerClass?: string;
footerClass?: string; footerClass?: string;
} }
@ -30,13 +27,7 @@ defineOptions({
name: 'Page', name: 'Page',
}); });
const { const { autoContentHeight = false } = defineProps<Props>();
contentClass = '',
description = '',
autoContentHeight = false,
title = '',
fixedHeader = false,
} = defineProps<Props>();
const headerHeight = ref(0); const headerHeight = ref(0);
const footerHeight = ref(0); const footerHeight = ref(0);
@ -45,24 +36,11 @@ const shouldAutoHeight = ref(false);
const headerRef = useTemplateRef<HTMLDivElement>('headerRef'); const headerRef = useTemplateRef<HTMLDivElement>('headerRef');
const footerRef = useTemplateRef<HTMLDivElement>('footerRef'); const footerRef = useTemplateRef<HTMLDivElement>('footerRef');
const headerStyle = computed<StyleValue>(() => { const contentStyle = computed<StyleValue>(() => {
return fixedHeader
? {
position: 'sticky',
zIndex: 200,
top:
preferences.header.mode === 'fixed' ? 'var(--vben-header-height)' : 0,
}
: undefined;
});
const contentStyle = computed(() => {
if (autoContentHeight) { if (autoContentHeight) {
return { return {
height: shouldAutoHeight.value height: `calc(var(${CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT}) - ${headerHeight.value}px)`,
? `calc(var(--vben-content-height) - ${headerHeight.value}px - ${footerHeight.value}px)` overflowY: shouldAutoHeight.value ? 'auto' : 'unset',
: '0',
// 'overflow-y': shouldAutoHeight.value?'auto':'unset',
}; };
} }
return {}; return {};
@ -73,9 +51,8 @@ async function calcContentHeight() {
return; return;
} }
await nextTick(); await nextTick();
const { getLayoutFooterHeight } = await useLayoutFooterStyle();
headerHeight.value = headerRef.value?.offsetHeight || 0; headerHeight.value = headerRef.value?.offsetHeight || 0;
footerHeight.value = getLayoutFooterHeight() || 0; footerHeight.value = footerRef.value?.offsetHeight || 0;
setTimeout(() => { setTimeout(() => {
shouldAutoHeight.value = true; shouldAutoHeight.value = true;
}, 30); }, 30);
@ -99,38 +76,31 @@ onMounted(() => {
ref="headerRef" ref="headerRef"
:class=" :class="
cn( cn(
'bg-card relative px-6 py-4', 'bg-card border-border relative flex items-end border-b px-6 py-4',
headerClass, headerClass,
fixedHeader
? 'border-border border-b transition-all duration-200'
: '',
) )
" "
:style="headerStyle"
> >
<slot name="title"> <div class="flex-auto">
<div v-if="title" class="mb-2 flex text-lg font-semibold"> <slot name="title">
{{ title }} <div v-if="title" class="mb-2 flex text-lg font-semibold">
</div> {{ title }}
</slot> </div>
</slot>
<slot name="description"> <slot name="description">
<p v-if="description" class="text-muted-foreground"> <p v-if="description" class="text-muted-foreground">
{{ description }} {{ description }}
</p> </p>
</slot> </slot>
</div>
<div v-if="$slots.extra" class="absolute bottom-4 right-4"> <div v-if="$slots.extra">
<slot name="extra"></slot> <slot name="extra"></slot>
</div> </div>
</div> </div>
<div <div :class="contentClass" :style="contentStyle" class="h-full p-4">
v-if="shouldAutoHeight"
:class="contentClass"
:style="contentStyle"
class="h-full p-4"
>
<slot></slot> <slot></slot>
</div> </div>
@ -139,8 +109,8 @@ onMounted(() => {
ref="footerRef" ref="footerRef"
:class=" :class="
cn( cn(
footerClass,
'bg-card align-center absolute bottom-0 left-0 right-0 flex px-6 py-4', 'bg-card align-center absolute bottom-0 left-0 right-0 flex px-6 py-4',
footerClass,
) )
" "
> >

View File

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

View File

@ -8,7 +8,7 @@ import type { BaseFormComponentType } from '@vben/common-ui';
import type { Component, SetupContext } from 'vue'; import type { Component, SetupContext } from 'vue';
import { h } 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 { $t } from '@vben/locales';
import { import {
@ -49,6 +49,7 @@ const withDefaultPlaceholder = <T extends Component>(
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明 // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType = export type ComponentType =
| 'ApiSelect' | 'ApiSelect'
| 'ApiTreeSelect'
| 'AutoComplete' | 'AutoComplete'
| 'Checkbox' | 'Checkbox'
| 'CheckboxGroup' | 'CheckboxGroup'
@ -82,13 +83,31 @@ async function initComponentAdapter() {
ApiSelect: (props, { attrs, slots }) => { ApiSelect: (props, { attrs, slots }) => {
return h( return h(
ApiSelect, ApiComponent,
{ {
placeholder: $t('ui.placeholder.select'),
...props, ...props,
...attrs, ...attrs,
component: Select, component: Select,
loadingSlot: 'suffixIcon', 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', visibleEvent: 'onVisibleChange',
}, },
slots, 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 BaseDemo from './base-demo.vue';
import DynamicDemo from './dynamic-demo.vue'; import DynamicDemo from './dynamic-demo.vue';
import FormDrawerDemo from './form-drawer-demo.vue'; import FormDrawerDemo from './form-drawer-demo.vue';
import inContentDemo from './in-content-demo.vue';
import SharedDataDemo from './shared-data-demo.vue'; import SharedDataDemo from './shared-data-demo.vue';
const [BaseDrawer, baseDrawerApi] = useVbenDrawer({ const [BaseDrawer, baseDrawerApi] = useVbenDrawer({
@ -16,6 +17,12 @@ const [BaseDrawer, baseDrawerApi] = useVbenDrawer({
// placement: 'left', // placement: 'left',
}); });
const [InContentDrawer, inContentDrawerApi] = useVbenDrawer({
//
connectedComponent: inContentDemo,
// placement: 'left',
});
const [AutoHeightDrawer, autoHeightDrawerApi] = useVbenDrawer({ const [AutoHeightDrawer, autoHeightDrawerApi] = useVbenDrawer({
connectedComponent: AutoHeightDemo, connectedComponent: AutoHeightDemo,
}); });
@ -37,6 +44,23 @@ function openBaseDrawer(placement: DrawerPlacement = 'right') {
baseDrawerApi.open(); 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() { function openAutoHeightDrawer() {
autoHeightDrawerApi.open(); autoHeightDrawerApi.open();
} }
@ -69,6 +93,7 @@ function openFormDrawer() {
<template> <template>
<Page <Page
auto-content-height
description="抽屉组件通常用于在当前页面上显示一个覆盖层,用以展示重要信息或提供用户交互界面。" description="抽屉组件通常用于在当前页面上显示一个覆盖层,用以展示重要信息或提供用户交互界面。"
title="抽屉组件示例" title="抽屉组件示例"
> >
@ -76,6 +101,7 @@ function openFormDrawer() {
<DocButton path="/components/common-ui/vben-drawer" /> <DocButton path="/components/common-ui/vben-drawer" />
</template> </template>
<BaseDrawer /> <BaseDrawer />
<InContentDrawer />
<AutoHeightDrawer /> <AutoHeightDrawer />
<DynamicDrawer /> <DynamicDrawer />
<SharedDataDrawer /> <SharedDataDrawer />
@ -83,18 +109,55 @@ function openFormDrawer() {
<Card class="mb-4" title="基本使用"> <Card class="mb-4" title="基本使用">
<p class="mb-3">一个基础的抽屉示例</p> <p class="mb-3">一个基础的抽屉示例</p>
<Button type="primary" @click="openBaseDrawer('right')"></Button> <Button class="mb-2" type="primary" @click="openBaseDrawer('right')">
<Button class="ml-2" type="primary" @click="openBaseDrawer('bottom')"> 右侧打开
</Button>
<Button
class="mb-2 ml-2"
type="primary"
@click="openBaseDrawer('bottom')"
>
底部打开 底部打开
</Button> </Button>
<Button class="ml-2" type="primary" @click="openBaseDrawer('left')"> <Button class="mb-2 ml-2" type="primary" @click="openBaseDrawer('left')">
左侧打开 左侧打开
</Button> </Button>
<Button class="ml-2" type="primary" @click="openBaseDrawer('top')"> <Button class="mb-2 ml-2" type="primary" @click="openBaseDrawer('top')">
顶部打开 顶部打开
</Button> </Button>
</Card> </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="内容高度自适应滚动"> <Card class="mb-4" title="内容高度自适应滚动">
<p class="mb-3">可根据内容自动计算滚动高度</p> <p class="mb-3">可根据内容自动计算滚动高度</p>
<Button type="primary" @click="openAutoHeightDrawer"></Button> <Button type="primary" @click="openAutoHeightDrawer"></Button>

View File

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

View File

@ -55,13 +55,28 @@ const [BaseForm, baseFormApi] = useVbenForm({
}, },
// //
api: getAllMenusApi, api: getAllMenusApi,
placeholder: '请选择',
}, },
// //
fieldName: 'api', fieldName: 'api',
// label // label
label: 'ApiSelect', label: 'ApiSelect',
}, },
{
component: 'ApiTreeSelect',
//
componentProps: {
//
api: getAllMenusApi,
childrenField: 'children',
// options
labelField: 'name',
valueField: 'path',
},
//
fieldName: 'apiTree',
// label
label: 'ApiTreeSelect',
},
{ {
component: 'InputPassword', component: 'InputPassword',
componentProps: { componentProps: {
@ -362,7 +377,6 @@ function handleSetFormValue() {
<Page <Page
content-class="flex flex-col gap-4" content-class="flex flex-col gap-4"
description="表单组件基础示例,请注意,该页面用到的参数代码会添加一些简单注释,方便理解,请仔细查看。" description="表单组件基础示例,请注意,该页面用到的参数代码会添加一些简单注释,方便理解,请仔细查看。"
fixed-header
header-class="pb-0" header-class="pb-0"
title="表单组件" title="表单组件"
> >

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

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,7 @@ packages:
- docs - docs
- playground - playground
catalog: catalog:
'@ast-grep/napi': ^0.31.0 '@ast-grep/napi': ^0.31.1
'@changesets/changelog-github': ^0.5.0 '@changesets/changelog-github': ^0.5.0
'@changesets/cli': ^2.27.10 '@changesets/cli': ^2.27.10
'@changesets/git': ^3.0.2 '@changesets/git': ^3.0.2
@ -23,20 +23,20 @@ catalog:
'@ctrl/tinycolor': ^4.1.0 '@ctrl/tinycolor': ^4.1.0
'@eslint/js': ^9.16.0 '@eslint/js': ^9.16.0
'@faker-js/faker': ^9.3.0 '@faker-js/faker': ^9.3.0
'@iconify/json': ^2.2.279 '@iconify/json': ^2.2.281
'@iconify/tailwind': ^1.1.3 '@iconify/tailwind': ^1.2.0
'@iconify/vue': ^4.1.2 '@iconify/vue': ^4.2.0
'@intlify/core-base': ^10.0.5 '@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 '@jspm/generator': ^2.4.1
'@manypkg/get-packages': ^2.2.2 '@manypkg/get-packages': ^2.2.2
'@nolebase/vitepress-plugin-git-changelog': ^2.11.1 '@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 '@pnpm/workspace.read-manifest': ^1000.0.0
'@stylistic/stylelint-plugin': ^3.1.1 '@stylistic/stylelint-plugin': ^3.1.1
'@tailwindcss/nesting': 0.0.0-insiders.565cd3e '@tailwindcss/nesting': 0.0.0-insiders.565cd3e
'@tailwindcss/typography': ^0.5.15 '@tailwindcss/typography': ^0.5.15
'@tanstack/vue-query': ^5.62.2 '@tanstack/vue-query': ^5.62.7
'@tanstack/vue-store': ^0.6.0 '@tanstack/vue-store': ^0.6.0
'@types/archiver': ^6.0.3 '@types/archiver': ^6.0.3
'@types/crypto-js': ^4.2.2 '@types/crypto-js': ^4.2.2
@ -52,8 +52,8 @@ catalog:
'@types/qrcode': ^1.5.5 '@types/qrcode': ^1.5.5
'@types/qs': ^6.9.17 '@types/qs': ^6.9.17
'@types/sortablejs': ^1.15.8 '@types/sortablejs': ^1.15.8
'@typescript-eslint/eslint-plugin': ^8.17.0 '@typescript-eslint/eslint-plugin': ^8.18.0
'@typescript-eslint/parser': ^8.17.0 '@typescript-eslint/parser': ^8.18.0
'@vee-validate/zod': ^4.14.7 '@vee-validate/zod': ^4.14.7
'@vite-pwa/vitepress': ^0.5.3 '@vite-pwa/vitepress': ^0.5.3
'@vitejs/plugin-vue': ^5.2.1 '@vitejs/plugin-vue': ^5.2.1
@ -93,9 +93,9 @@ catalog:
eslint-plugin-command: ^0.2.6 eslint-plugin-command: ^0.2.6
eslint-plugin-eslint-comments: ^3.2.0 eslint-plugin-eslint-comments: ^3.2.0
eslint-plugin-import-x: ^4.5.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-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-no-only-tests: ^3.3.0
eslint-plugin-perfectionist: ^3.9.1 eslint-plugin-perfectionist: ^3.9.1
eslint-plugin-prettier: ^5.2.1 eslint-plugin-prettier: ^5.2.1
@ -104,7 +104,7 @@ catalog:
eslint-plugin-unused-imports: ^4.1.4 eslint-plugin-unused-imports: ^4.1.4
eslint-plugin-vitest: ^0.5.4 eslint-plugin-vitest: ^0.5.4
eslint-plugin-vue: ^9.32.0 eslint-plugin-vue: ^9.32.0
execa: ^9.5.1 execa: ^9.5.2
find-up: ^7.0.0 find-up: ^7.0.0
get-port: ^7.1.0 get-port: ^7.1.0
globals: ^15.13.0 globals: ^15.13.0
@ -116,7 +116,7 @@ catalog:
is-ci: ^3.0.1 is-ci: ^3.0.1
jsonc-eslint-parser: ^2.4.0 jsonc-eslint-parser: ^2.4.0
jsonwebtoken: ^9.0.2 jsonwebtoken: ^9.0.2
lint-staged: ^15.2.10 lint-staged: ^15.2.11
lodash.clonedeep: ^4.5.0 lodash.clonedeep: ^4.5.0
lodash.get: ^4.4.2 lodash.get: ^4.4.2
lodash.isequal: ^4.5.0 lodash.isequal: ^4.5.0
@ -129,7 +129,7 @@ catalog:
pinia: 2.2.2 pinia: 2.2.2
pinia-plugin-persistedstate: ^4.1.3 pinia-plugin-persistedstate: ^4.1.3
pkg-types: ^1.2.1 pkg-types: ^1.2.1
playwright: ^1.49.0 playwright: ^1.49.1
postcss: ^8.4.49 postcss: ^8.4.49
postcss-antd-fixes: ^0.2.0 postcss-antd-fixes: ^0.2.0
postcss-html: ^1.7.0 postcss-html: ^1.7.0
@ -144,7 +144,7 @@ catalog:
radix-vue: ^1.9.10 radix-vue: ^1.9.10
resolve.exports: ^2.0.3 resolve.exports: ^2.0.3
rimraf: ^6.0.1 rimraf: ^6.0.1
rollup: ^4.28.0 rollup: ^4.28.1
rollup-plugin-visualizer: ^5.12.0 rollup-plugin-visualizer: ^5.12.0
sass: 1.80.6 sass: 1.80.6
sortablejs: ^1.15.6 sortablejs: ^1.15.6
@ -166,7 +166,7 @@ catalog:
unbuild: ^3.0.0-rc.11 unbuild: ^3.0.0-rc.11
unplugin-element-plus: ^0.8.0 unplugin-element-plus: ^0.8.0
vee-validate: ^4.14.7 vee-validate: ^4.14.7
vite: ^6.0.2 vite: ^6.0.3
vite-plugin-compression: ^0.5.1 vite-plugin-compression: ^0.5.1
vite-plugin-dts: 4.2.1 vite-plugin-dts: 4.2.1
vite-plugin-html: ^3.2.2 vite-plugin-html: ^3.2.2
@ -182,8 +182,8 @@ catalog:
vue-i18n: ^10.0.5 vue-i18n: ^10.0.5
vue-router: ^4.5.0 vue-router: ^4.5.0
vue-tsc: ^2.1.10 vue-tsc: ^2.1.10
vxe-pc-ui: ^4.3.14 vxe-pc-ui: ^4.3.27
vxe-table: ^4.9.14 vxe-table: ^4.9.23
watermark-js-plus: ^1.5.7 watermark-js-plus: ^1.5.7
zod: ^3.23.8 zod: ^3.24.1
zod-defaults: ^0.1.3 zod-defaults: ^0.1.3