Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev-v5
commit
eb0d43e26c
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ setupVbenForm<ComponentType>({
|
||||||
config: {
|
config: {
|
||||||
modelPropNameMap: {
|
modelPropNameMap: {
|
||||||
Upload: 'fileList',
|
Upload: 'fileList',
|
||||||
|
CheckboxGroup: 'model-value',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defineRules: {
|
defineRules: {
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"title": "演示",
|
"title": "演示",
|
||||||
"elementPlus": "Element Plus",
|
"elementPlus": "Element Plus",
|
||||||
|
"form": "表单演示",
|
||||||
"vben": {
|
"vben": {
|
||||||
"title": "项目",
|
"title": "项目",
|
||||||
"about": "关于",
|
"about": "关于",
|
||||||
|
|
|
||||||
|
|
@ -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'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
// 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个
|
||||||
|
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>
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
"title": "演示",
|
"title": "演示",
|
||||||
"naive": "Naive UI",
|
"naive": "Naive UI",
|
||||||
"table": "Table",
|
"table": "Table",
|
||||||
|
"form": "表单",
|
||||||
"vben": {
|
"vben": {
|
||||||
"title": "项目",
|
"title": "项目",
|
||||||
"about": "关于",
|
"about": "关于",
|
||||||
|
|
|
||||||
|
|
@ -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'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
// 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个
|
||||||
|
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>
|
||||||
|
|
@ -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 模态框',
|
||||||
|
|
|
||||||
|
|
@ -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` | - |
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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` |
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 注意
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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 默认命名空间
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
class="ml-3"
|
||||||
type="button"
|
type="button"
|
||||||
@click="handleSubmit"
|
@click="handleSubmit"
|
||||||
v-bind="submitButtonOptions"
|
v-bind="submitButtonOptions"
|
||||||
>
|
>
|
||||||
{{ submitButtonOptions.content }}
|
{{ submitButtonOptions.content }}
|
||||||
</component>
|
</component>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- 展开按钮前 -->
|
<!-- 展开按钮前 -->
|
||||||
<slot name="expand-before"></slot>
|
<slot name="expand-before"></slot>
|
||||||
|
|
|
||||||
|
|
@ -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类型的值。
|
||||||
|
* antd的日期时间相关组件的值类型为dayjs对象
|
||||||
|
* element-plus的日期时间相关组件的值类型可能为Date对象
|
||||||
|
* 以上两种类型需要排除深度合并
|
||||||
|
*/
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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="{
|
||||||
|
|
|
||||||
|
|
@ -318,6 +318,10 @@ export interface VbenFormProps<
|
||||||
FormRenderProps<T>,
|
FormRenderProps<T>,
|
||||||
'componentBindEventMap' | 'componentMap' | 'form'
|
'componentBindEventMap' | 'componentMap' | 'form'
|
||||||
> {
|
> {
|
||||||
|
/**
|
||||||
|
* 操作按钮是否反转(提交按钮前置)
|
||||||
|
*/
|
||||||
|
actionButtonsReverse?: boolean;
|
||||||
/**
|
/**
|
||||||
* 表单操作区域class
|
* 表单操作区域class
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
||||||
|
|
|
||||||
|
|
@ -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:"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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()"
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as ApiComponent } from './api-component.vue';
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export { default as ApiSelect } from './api-select.vue';
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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,15 +76,12 @@ 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"
|
|
||||||
>
|
>
|
||||||
|
<div class="flex-auto">
|
||||||
<slot name="title">
|
<slot name="title">
|
||||||
<div v-if="title" class="mb-2 flex text-lg font-semibold">
|
<div v-if="title" class="mb-2 flex text-lg font-semibold">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
|
|
@ -119,18 +93,14 @@ onMounted(() => {
|
||||||
{{ 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,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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') {
|
||||||
|
// 页面顶部区域的层级只有200,所以设置一个低于200的值,抽屉从顶部滑出来的时候才比较合适
|
||||||
|
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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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="表单组件"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
845
pnpm-lock.yaml
845
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue