!73 vben 最新同步(04-14)

Merge pull request !73 from 芋道源码/vben-sync
pull/75/MERGE
芋道源码 2025-04-14 11:43:00 +00:00 committed by Gitee
commit fe240b01df
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
55 changed files with 2476 additions and 1334 deletions

13
.vscode/settings.json vendored
View File

@ -223,5 +223,16 @@
"commentTranslate.multiLineMerge": true,
"vue.server.hybridMode": true,
"typescript.tsdk": "node_modules/typescript/lib",
"oxc.enable": false
"oxc.enable": false,
"cSpell.words": [
"archiver",
"axios",
"dotenv",
"isequal",
"jspm",
"napi",
"nolebase",
"rollup",
"vitest"
]
}

View File

@ -8,35 +8,64 @@ import type { Component } from 'vue';
import type { BaseFormComponentType } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import { defineComponent, getCurrentInstance, h, ref } from 'vue';
import {
defineAsyncComponent,
defineComponent,
getCurrentInstance,
h,
ref,
} from 'vue';
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales';
import {
AutoComplete,
Button,
Checkbox,
CheckboxGroup,
DatePicker,
Divider,
Input,
InputNumber,
InputPassword,
Mentions,
notification,
Radio,
RadioGroup,
RangePicker,
Rate,
Select,
Space,
Switch,
Textarea,
TimePicker,
TreeSelect,
Upload,
} from 'ant-design-vue';
import { notification } from 'ant-design-vue';
const AutoComplete = defineAsyncComponent(
() => import('ant-design-vue/es/auto-complete'),
);
const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
const Checkbox = defineAsyncComponent(
() => import('ant-design-vue/es/checkbox'),
);
const CheckboxGroup = defineAsyncComponent(() =>
import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup),
);
const DatePicker = defineAsyncComponent(
() => import('ant-design-vue/es/date-picker'),
);
const Divider = defineAsyncComponent(() => import('ant-design-vue/es/divider'));
const Input = defineAsyncComponent(() => import('ant-design-vue/es/input'));
const InputNumber = defineAsyncComponent(
() => import('ant-design-vue/es/input-number'),
);
const InputPassword = defineAsyncComponent(() =>
import('ant-design-vue/es/input').then((res) => res.InputPassword),
);
const Mentions = defineAsyncComponent(
() => import('ant-design-vue/es/mentions'),
);
const Radio = defineAsyncComponent(() => import('ant-design-vue/es/radio'));
const RadioGroup = defineAsyncComponent(() =>
import('ant-design-vue/es/radio').then((res) => res.RadioGroup),
);
const RangePicker = defineAsyncComponent(() =>
import('ant-design-vue/es/date-picker').then((res) => res.RangePicker),
);
const Rate = defineAsyncComponent(() => import('ant-design-vue/es/rate'));
const Select = defineAsyncComponent(() => import('ant-design-vue/es/select'));
const Space = defineAsyncComponent(() => import('ant-design-vue/es/space'));
const Switch = defineAsyncComponent(() => import('ant-design-vue/es/switch'));
const Textarea = defineAsyncComponent(() =>
import('ant-design-vue/es/input').then((res) => res.Textarea),
);
const TimePicker = defineAsyncComponent(
() => import('ant-design-vue/es/time-picker'),
);
const TreeSelect = defineAsyncComponent(
() => import('ant-design-vue/es/tree-select'),
);
const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));
const withDefaultPlaceholder = <T extends Component>(
component: T,

View File

@ -1,8 +1,7 @@
import { createApp, watchEffect } from 'vue';
import { registerAccessDirective } from '@vben/access';
import { initTippy, registerLoadingDirective } from '@vben/common-ui';
import { MotionPlugin } from '@vben/plugins/motion';
import { registerLoadingDirective } from '@vben/common-ui/es/loading';
import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores';
import '@vben/styles';
@ -47,12 +46,14 @@ async function bootstrap(namespace: string) {
registerAccessDirective(app);
// 初始化 tippy
const { initTippy } = await import('@vben/common-ui/es/tippy');
initTippy(app);
// 配置路由及路由守卫
app.use(router);
// 配置Motion插件
const { MotionPlugin } = await import('@vben/plugins/motion');
app.use(MotionPlugin);
// 动态更新标题

View File

@ -2,10 +2,10 @@ import type { RouteRecordRaw } from 'vue-router';
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
import { AuthPageLayout, BasicLayout } from '#/layouts';
import { $t } from '#/locales';
import Login from '#/views/_core/authentication/login.vue';
const BasicLayout = () => import('#/layouts/basic.vue');
const AuthPageLayout = () => import('#/layouts/auth.vue');
/** 全局404页面 */
const fallbackNotFoundRoute: RouteRecordRaw = {
component: () => import('#/views/_core/fallback/not-found.vue'),
@ -50,7 +50,7 @@ const coreRoutes: RouteRecordRaw[] = [
{
name: 'Login',
path: 'login',
component: Login,
component: () => import('#/views/_core/authentication/login.vue'),
meta: {
title: $t('page.auth.login'),
},

View File

@ -0,0 +1,46 @@
import dayjs from 'dayjs';
/** 时间段选择器拓展 */
export const rangePickerExtend = () => {
return {
showTime: {
format: 'HH:mm:ss',
defaultValue: [
dayjs('00:00:00', 'HH:mm:ss'),
dayjs('23:59:59', 'HH:mm:ss'),
],
},
// 如果需要10位时间戳秒级可以使用 valueFormat: 'X'
valueFormat: 'YYYY-MM-DD HH:mm:ss',
format: 'YYYY-MM-DD HH:mm:ss', // 显示格式
placeholder: ['开始时间', '结束时间'],
ranges: {
: [dayjs().startOf('day'), dayjs().endOf('day')],
: [
dayjs().subtract(1, 'day').startOf('day'),
dayjs().subtract(1, 'day').endOf('day'),
],
: [dayjs().startOf('week'), dayjs().endOf('day')],
: [dayjs().startOf('month'), dayjs().endOf('day')],
7: [
dayjs().subtract(7, 'day').startOf('day'),
dayjs().endOf('day'),
],
30: [
dayjs().subtract(30, 'day').startOf('day'),
dayjs().endOf('day'),
],
},
transformDateFunc: (dates: any) => {
if (dates && dates.length === 2) {
return [dates.createTime[0], dates.createTime[1]].join(','); // 格式化为后台支持的时间格式
}
return {};
},
};
};

View File

@ -8,31 +8,121 @@ import type { Component } from 'vue';
import type { BaseFormComponentType } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import { defineComponent, getCurrentInstance, h, ref } from 'vue';
import {
defineAsyncComponent,
defineComponent,
getCurrentInstance,
h,
ref,
} from 'vue';
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales';
import {
ElButton,
ElCheckbox,
ElCheckboxButton,
ElCheckboxGroup,
ElDatePicker,
ElDivider,
ElInput,
ElInputNumber,
ElNotification,
ElRadio,
ElRadioButton,
ElRadioGroup,
ElSelectV2,
ElSpace,
ElSwitch,
ElTimePicker,
ElTreeSelect,
ElUpload,
} from 'element-plus';
import { ElNotification } from 'element-plus';
const ElButton = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/button/index'),
import('element-plus/es/components/button/style/css'),
]).then(([res]) => res.ElButton),
);
const ElCheckbox = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/checkbox/index'),
import('element-plus/es/components/checkbox/style/css'),
]).then(([res]) => res.ElCheckbox),
);
const ElCheckboxButton = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/checkbox/index'),
import('element-plus/es/components/checkbox-button/style/css'),
]).then(([res]) => res.ElCheckboxButton),
);
const ElCheckboxGroup = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/checkbox/index'),
import('element-plus/es/components/checkbox-group/style/css'),
]).then(([res]) => res.ElCheckboxGroup),
);
const ElDatePicker = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/date-picker/index'),
import('element-plus/es/components/date-picker/style/css'),
]).then(([res]) => res.ElDatePicker),
);
const ElDivider = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/divider/index'),
import('element-plus/es/components/divider/style/css'),
]).then(([res]) => res.ElDivider),
);
const ElInput = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/input/index'),
import('element-plus/es/components/input/style/css'),
]).then(([res]) => res.ElInput),
);
const ElInputNumber = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/input-number/index'),
import('element-plus/es/components/input-number/style/css'),
]).then(([res]) => res.ElInputNumber),
);
const ElRadio = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/radio/index'),
import('element-plus/es/components/radio/style/css'),
]).then(([res]) => res.ElRadio),
);
const ElRadioButton = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/radio/index'),
import('element-plus/es/components/radio-button/style/css'),
]).then(([res]) => res.ElRadioButton),
);
const ElRadioGroup = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/radio/index'),
import('element-plus/es/components/radio-group/style/css'),
]).then(([res]) => res.ElRadioGroup),
);
const ElSelectV2 = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/select-v2/index'),
import('element-plus/es/components/select-v2/style/css'),
]).then(([res]) => res.ElSelectV2),
);
const ElSpace = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/space/index'),
import('element-plus/es/components/space/style/css'),
]).then(([res]) => res.ElSpace),
);
const ElSwitch = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/switch/index'),
import('element-plus/es/components/switch/style/css'),
]).then(([res]) => res.ElSwitch),
);
const ElTimePicker = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/time-picker/index'),
import('element-plus/es/components/time-picker/style/css'),
]).then(([res]) => res.ElTimePicker),
);
const ElTreeSelect = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/tree-select/index'),
import('element-plus/es/components/tree-select/style/css'),
]).then(([res]) => res.ElTreeSelect),
);
const ElUpload = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/upload/index'),
import('element-plus/es/components/upload/style/css'),
]).then(([res]) => res.ElUpload),
);
const withDefaultPlaceholder = <T extends Component>(
component: T,

View File

@ -1,8 +1,7 @@
import { createApp, watchEffect } from 'vue';
import { registerAccessDirective } from '@vben/access';
import { initTippy, registerLoadingDirective } from '@vben/common-ui';
import { MotionPlugin } from '@vben/plugins/motion';
import { registerLoadingDirective } from '@vben/common-ui';
import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores';
import '@vben/styles';
@ -49,12 +48,14 @@ async function bootstrap(namespace: string) {
registerAccessDirective(app);
// 初始化 tippy
const { initTippy } = await import('@vben/common-ui/es/tippy');
initTippy(app);
// 配置路由及路由守卫
app.use(router);
// 配置Motion插件
const { MotionPlugin } = await import('@vben/plugins/motion');
app.use(MotionPlugin);
// 动态更新标题

View File

@ -2,10 +2,10 @@ import type { RouteRecordRaw } from 'vue-router';
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
import { AuthPageLayout, BasicLayout } from '#/layouts';
import { $t } from '#/locales';
import Login from '#/views/_core/authentication/login.vue';
const BasicLayout = () => import('#/layouts/basic.vue');
const AuthPageLayout = () => import('#/layouts/auth.vue');
/** 全局404页面 */
const fallbackNotFoundRoute: RouteRecordRaw = {
component: () => import('#/views/_core/fallback/not-found.vue'),
@ -50,7 +50,7 @@ const coreRoutes: RouteRecordRaw[] = [
{
name: 'Login',
path: 'login',
component: Login,
component: () => import('#/views/_core/authentication/login.vue'),
meta: {
title: $t('page.auth.login'),
},

View File

@ -1,7 +1,7 @@
<script lang="ts" setup>
import { h } from 'vue';
import { Page } from '@vben/common-ui';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { ElButton, ElCard, ElCheckbox, ElMessage } from 'element-plus';
@ -17,11 +17,16 @@ const [Form, formApi] = useVbenForm({
},
layout: 'horizontal',
// 321
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
// wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
handleSubmit: (values) => {
ElMessage.success(`表单数据:${JSON.stringify(values)}`);
},
schema: [
{
component: 'IconPicker',
fieldName: 'icon',
label: 'IconPicker',
},
{
// #/adapter.ts
component: 'ApiSelect',
@ -149,6 +154,8 @@ const [Form, formApi] = useVbenForm({
},
],
});
const [Drawer, drawerApi] = useVbenDrawer();
function setFormValues() {
formApi.setValues({
string: 'string',
@ -168,6 +175,9 @@ function setFormValues() {
description="我们重新包装了CheckboxGroup、RadioGroup、Select可以通过options属性传入选项属性数组以自动生成选项"
title="表单演示"
>
<Drawer class="w-[600px]" title="基础表单示例">
<Form />
</Drawer>
<ElCard>
<template #header>
<div class="flex items-center">
@ -175,7 +185,7 @@ function setFormValues() {
<ElButton type="primary" @click="setFormValues"></ElButton>
</div>
</template>
<Form />
<ElButton type="primary" @click="drawerApi.open"> </ElButton>
</ElCard>
</Page>
</template>

View File

@ -8,32 +8,68 @@ import type { Component } from 'vue';
import type { BaseFormComponentType } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import { defineComponent, getCurrentInstance, h, ref } from 'vue';
import {
defineAsyncComponent,
defineComponent,
getCurrentInstance,
h,
ref,
} from 'vue';
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales';
import {
NButton,
NCheckbox,
NCheckboxGroup,
NDatePicker,
NDivider,
NInput,
NInputNumber,
NRadio,
NRadioButton,
NRadioGroup,
NSelect,
NSpace,
NSwitch,
NTimePicker,
NTreeSelect,
NUpload,
} from 'naive-ui';
import { message } from '#/adapter/naive';
const NButton = defineAsyncComponent(() =>
import('naive-ui/es/button').then((res) => res.NButton),
);
const NCheckbox = defineAsyncComponent(() =>
import('naive-ui/es/checkbox').then((res) => res.NCheckbox),
);
const NCheckboxGroup = defineAsyncComponent(() =>
import('naive-ui/es/checkbox').then((res) => res.NCheckboxGroup),
);
const NDatePicker = defineAsyncComponent(() =>
import('naive-ui/es/date-picker').then((res) => res.NDatePicker),
);
const NDivider = defineAsyncComponent(() =>
import('naive-ui/es/divider').then((res) => res.NDivider),
);
const NInput = defineAsyncComponent(() =>
import('naive-ui/es/input').then((res) => res.NInput),
);
const NInputNumber = defineAsyncComponent(() =>
import('naive-ui/es/input-number').then((res) => res.NInputNumber),
);
const NRadio = defineAsyncComponent(() =>
import('naive-ui/es/radio').then((res) => res.NRadio),
);
const NRadioButton = defineAsyncComponent(() =>
import('naive-ui/es/radio').then((res) => res.NRadioButton),
);
const NRadioGroup = defineAsyncComponent(() =>
import('naive-ui/es/radio').then((res) => res.NRadioGroup),
);
const NSelect = defineAsyncComponent(() =>
import('naive-ui/es/select').then((res) => res.NSelect),
);
const NSpace = defineAsyncComponent(() =>
import('naive-ui/es/space').then((res) => res.NSpace),
);
const NSwitch = defineAsyncComponent(() =>
import('naive-ui/es/switch').then((res) => res.NSwitch),
);
const NTimePicker = defineAsyncComponent(() =>
import('naive-ui/es/time-picker').then((res) => res.NTimePicker),
);
const NTreeSelect = defineAsyncComponent(() =>
import('naive-ui/es/tree-select').then((res) => res.NTreeSelect),
);
const NUpload = defineAsyncComponent(() =>
import('naive-ui/es/upload').then((res) => res.NUpload),
);
const withDefaultPlaceholder = <T extends Component>(
component: T,
type: 'input' | 'select',

View File

@ -1,8 +1,7 @@
import { createApp, watchEffect } from 'vue';
import { registerAccessDirective } from '@vben/access';
import { initTippy, registerLoadingDirective } from '@vben/common-ui';
import { MotionPlugin } from '@vben/plugins/motion';
import { registerLoadingDirective } from '@vben/common-ui';
import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores';
import '@vben/styles';
@ -47,12 +46,14 @@ async function bootstrap(namespace: string) {
registerAccessDirective(app);
// 初始化 tippy
const { initTippy } = await import('@vben/common-ui/es/tippy');
initTippy(app);
// 配置路由及路由守卫
app.use(router);
// 配置Motion插件
const { MotionPlugin } = await import('@vben/plugins/motion');
app.use(MotionPlugin);
// 动态更新标题

View File

@ -2,10 +2,10 @@ import type { RouteRecordRaw } from 'vue-router';
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
import { AuthPageLayout, BasicLayout } from '#/layouts';
import { $t } from '#/locales';
import Login from '#/views/_core/authentication/login.vue';
const BasicLayout = () => import('#/layouts/basic.vue');
const AuthPageLayout = () => import('#/layouts/auth.vue');
/** 全局404页面 */
const fallbackNotFoundRoute: RouteRecordRaw = {
component: () => import('#/views/_core/fallback/not-found.vue'),
@ -50,7 +50,7 @@ const coreRoutes: RouteRecordRaw[] = [
{
name: 'Login',
path: 'login',
component: Login,
component: () => import('#/views/_core/authentication/login.vue'),
meta: {
title: $t('page.auth.login'),
},

View File

@ -104,6 +104,11 @@
--vp-custom-block-tip-text: var(--vp-c-text-1);
--vp-custom-block-tip-bg: var(--vp-c-brand-soft);
--vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
/**
* modal zIndex
*/
--popup-z-index: 1000;
}
@media (min-width: 640px) {

View File

@ -43,6 +43,9 @@ export type BeforeCloseScope = {
isConfirm: boolean;
};
/**
* alert 属性
*/
export type AlertProps = {
/** 关闭前的回调如果返回false则终止关闭 */
beforeClose?: (
@ -50,6 +53,8 @@ export type AlertProps = {
) => boolean | Promise<boolean | undefined> | undefined;
/** 边框 */
bordered?: boolean;
/** 按钮对齐方式 */
buttonAlign?: 'center' | 'end' | 'start';
/** 取消按钮的标题 */
cancelText?: string;
/** 是否居中显示 */
@ -62,14 +67,41 @@ export type AlertProps = {
content: Component | string;
/** 弹窗内容的额外样式 */
contentClass?: string;
/** 执行beforeClose回调期间在内容区域显示一个loading遮罩*/
contentMasking?: boolean;
/** 弹窗底部内容(与按钮在同一个容器中) */
footer?: Component | string;
/** 弹窗的图标(在标题的前面) */
icon?: Component | IconType;
/**
* 弹窗遮罩模糊效果
*/
overlayBlur?: number;
/** 是否显示取消按钮 */
showCancel?: boolean;
/** 弹窗标题 */
title?: string;
};
/** prompt 属性 */
export type PromptProps<T = any> = {
/** 关闭前的回调如果返回false则终止关闭 */
beforeClose?: (scope: {
isConfirm: boolean;
value: T | undefined;
}) => boolean | Promise<boolean | undefined> | undefined;
/** 用于接受用户输入的组件 */
component?: Component;
/** 输入组件的属性 */
componentProps?: Recordable<any>;
/** 输入组件的插槽 */
componentSlots?: Recordable<Component>;
/** 默认值 */
defaultValue?: T;
/** 输入组件的值属性名 */
modelPropName?: string;
} & Omit<AlertProps, 'beforeClose'>;
/**
* 函数签名
* alert和confirm的函数签名相同。

View File

@ -131,26 +131,37 @@ function fetchApi(): Promise<Record<string, any>> {
### Props
| 属性名 | 描述 | 类型 | 默认值 |
| --- | --- | --- | --- |
| modelValue(v-model) | 当前值 | `any` | - |
| 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` | - |
| 属性名 | 描述 | 类型 | 默认值 | 版本要求 |
| --- | --- | --- | --- | --- |
| modelValue(v-model) | 当前值 | `any` | - | - |
| 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` | - | - |
| autoSelect | 自动设置选项 | `'first' \| 'last' \| 'one'\| (item: OptionsItem[]) => OptionsItem \| false` | `false` | >5.5.4 |
#### autoSelect 自动设置选项
如果当前值为undefined在选项数据成功加载之后自动从备选项中选择一个作为当前值。默认值为`false`,即不自动选择选项。注意:该属性不应用于多选组件。可选值有:
- `first`:自动选择第一个选项
- `last`:自动选择最后一个选项
- `one`:有且仅有一个选项时,自动选择它
- `函数`自定义选择逻辑函数的参数为options返回值为选择的选项
- false不自动选择选项
### Methods

View File

@ -60,7 +60,6 @@ Modal 内的内容一般业务中,会比较复杂,所以我们可以将 moda
- `VbenModal` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenModal参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
- 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenModal`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。
- 使用了`connectedComponent`参数时,可以配置`destroyOnClose`属性来决定当关闭弹窗时,是否要销毁`connectedComponent`组件(重新创建`connectedComponent`组件,这将会把其内部所有的变量、状态、数据等恢复到初始状态。)。
- 如果弹窗的默认行为不符合你的预期,可以在`src\bootstrap.ts`中修改`setDefaultModalProps`的参数来设置默认的属性如默认隐藏全屏按钮修改默认ZIndex等。
:::
@ -84,7 +83,7 @@ const [Modal, modalApi] = useVbenModal({
| --- | --- | --- | --- |
| appendToMain | 是否挂载到内容区域默认挂载到body | `boolean` | `false` |
| connectedComponent | 连接另一个Modal组件 | `Component` | - |
| destroyOnClose | 关闭时销毁`connectedComponent` | `boolean` | `false` |
| destroyOnClose | 关闭时销毁 | `boolean` | `false` |
| title | 标题 | `string\|slot` | - |
| titleTooltip | 标题提示信息 | `string\|slot` | - |
| description | 描述信息 | `string\|slot` | - |

View File

@ -167,6 +167,23 @@ vxeUI.renderer.add('CellLink', {
当启用了表单搜索时可以在toolbarConfig中配置`search`为`true`来让表格在工具栏区域显示一个搜索表单控制按钮。表格的所有以`form-`开头的命名插槽都会被传递给搜索表单。
### 定制分隔条
当你启用表单搜索时在表单和表格之间会显示一个分隔条。这个分隔条使用了默认的组件背景色并且横向贯穿整个Vben Vxe Table在视觉上融入了页面的默认背景中。如果你在Vben Vxe Table的外层包裹了一个不同背景色的容器如将其放在一个Card内默认的表单和表格之间的分隔条可能就显得格格不入了下面的代码演示了如何定制这个分隔条。
```ts
const [Grid] = useVbenVxeGrid({
formOptions: {},
gridOptions: {},
// 完全移除分隔条
separator: false,
// 你也可以使用下面的代码来移除分隔条
// separator: { show: false },
// 或者使用下面的代码来改变分隔条的颜色
// separator: { backgroundColor: 'rgba(100,100,0,0.5)' },
});
```
<DemoPreview dir="demos/vben-vxe-table/form" />
## 单元格编辑
@ -231,15 +248,16 @@ useVbenVxeGrid 返回的第二个参数,是一个对象,包含了一些表
所有属性都可以传入 `useVbenVxeGrid` 的第一个参数中。
| 属性名 | 描述 | 类型 |
| -------------- | -------------------- | ------------------- |
| tableTitle | 表格标题 | `string` |
| tableTitleHelp | 表格标题帮助信息 | `string` |
| gridClass | grid组件的class | `string` |
| gridOptions | grid组件的参数 | `VxeTableGridProps` |
| gridEvents | grid组件的触发的事件 | `VxeGridListeners` |
| formOptions | 表单参数 | `VbenFormProps` |
| showSearchForm | 是否显示搜索表单 | `boolean` |
| 属性名 | 描述 | 类型 | 版本要求 |
| --- | --- | --- | --- |
| tableTitle | 表格标题 | `string` | - |
| tableTitleHelp | 表格标题帮助信息 | `string` | - |
| gridClass | grid组件的class | `string` | - |
| gridOptions | grid组件的参数 | `VxeTableGridProps` | - |
| gridEvents | grid组件的触发的事件 | `VxeGridListeners` | - |
| formOptions | 表单参数 | `VbenFormProps` | - |
| showSearchForm | 是否显示搜索表单 | `boolean` | - |
| separator | 搜索表单与表格主体之间的分隔条 | `boolean\|SeparatorOptions` | >5.5.4 |
## Slots

View File

@ -3,7 +3,7 @@ import { h } from 'vue';
import { alert, VbenButton } from '@vben/common-ui';
import { Empty } from 'ant-design-vue';
import { Result } from 'ant-design-vue';
function showAlert() {
alert('This is an alert message');
@ -18,7 +18,12 @@ function showIconAlert() {
function showCustomAlert() {
alert({
content: h(Empty, { description: '什么都没有' }),
buttonAlign: 'center',
content: h(Result, {
status: 'success',
subTitle: '已成功创建订单。订单ID2017182818828182881',
title: '操作成功',
}),
});
}
</script>

View File

@ -1,6 +1,10 @@
<script lang="ts" setup>
import { h, ref } from 'vue';
import { alert, confirm, VbenButton } from '@vben/common-ui';
import { Checkbox, message } from 'ant-design-vue';
function showConfirm() {
confirm('This is an alert message')
.then(() => {
@ -18,6 +22,34 @@ function showIconConfirm() {
});
}
function showfooterConfirm() {
const checked = ref(false);
confirm({
cancelText: '不要虾扯蛋',
confirmText: '是的我们都是NPC',
content:
'刚才发生的事情,为什么我似乎早就经历过一般?\n我甚至能在事情发生过程中潜意识里预知到接下来会发生什么。\n\n听起来挺玄乎的你有过这种感觉吗',
footer: () =>
h(
Checkbox,
{
checked: checked.value,
class: 'flex-1',
'onUpdate:checked': (v) => (checked.value = v),
},
'不再提示',
),
icon: 'question',
title: '未解之谜',
}).then(() => {
if (checked.value) {
message.success('我不会再拿这个问题烦你了');
} else {
message.info('下次还要继续问你哟');
}
});
}
function showAsyncConfirm() {
confirm({
beforeClose({ isConfirm }) {
@ -37,6 +69,7 @@ function showAsyncConfirm() {
<div class="flex gap-4">
<VbenButton @click="showConfirm">Confirm</VbenButton>
<VbenButton @click="showIconConfirm">Confirm With Icon</VbenButton>
<VbenButton @click="showfooterConfirm">Confirm With Footer</VbenButton>
<VbenButton @click="showAsyncConfirm">Async Confirm</VbenButton>
</div>
</template>

View File

@ -1,7 +1,10 @@
<script lang="ts" setup>
import { h } from 'vue';
import { alert, prompt, VbenButton } from '@vben/common-ui';
import { VbenSelect } from '@vben-core/shadcn-ui';
import { Input, RadioGroup, Select } from 'ant-design-vue';
import { BadgeJapaneseYen } from 'lucide-vue-next';
function showPrompt() {
prompt({
@ -15,27 +18,87 @@ function showPrompt() {
});
}
function showSlotsPrompt() {
prompt({
component: Input,
componentProps: {
placeholder: '请输入',
prefix: '充值金额',
type: 'number',
},
componentSlots: {
addonAfter: () => h(BadgeJapaneseYen),
},
content: '此弹窗演示了如何使用componentSlots传递自定义插槽',
icon: 'question',
modelPropName: 'value',
}).then((val) => {
if (val) alert(`你输入的是${val}`);
});
}
function showSelectPrompt() {
prompt({
component: VbenSelect,
component: Select,
componentProps: {
options: [
{ label: 'Option A', value: 'Option A' },
{ label: 'Option B', value: 'Option B' },
{ label: 'Option C', value: 'Option C' },
],
placeholder: '请选择',
// bodypointer-eventsnone
popupClassName: 'pointer-events-auto',
},
content: '此弹窗演示了如何使用component传递自定义组件',
icon: 'question',
modelPropName: 'value',
}).then((val) => {
if (val) {
alert(`你选择了${val}`);
}
});
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function showAsyncPrompt() {
prompt({
async beforeClose(scope) {
if (scope.isConfirm) {
if (scope.value) {
// false
await sleep(2000);
} else {
alert('请选择一个选项');
return false;
}
}
},
component: RadioGroup,
componentProps: {
class: 'flex flex-col',
options: [
{ label: 'Option 1', value: 'option1' },
{ label: 'Option 2', value: 'option2' },
{ label: 'Option 3', value: 'option3' },
],
placeholder: '请选择',
},
content: 'This is an alert message with icon',
content: '选择一个选项后再点击[确认]',
icon: 'question',
modelPropName: 'value',
}).then((val) => {
alert(`你选择的是${val}`);
alert(`${val} 已设置。`);
});
}
</script>
<template>
<div class="flex gap-4">
<VbenButton @click="showPrompt">Prompt</VbenButton>
<VbenButton @click="showSelectPrompt">Confirm With Select</VbenButton>
<VbenButton @click="showSlotsPrompt"> Prompt With slots </VbenButton>
<VbenButton @click="showSelectPrompt">Prompt With Select</VbenButton>
<VbenButton @click="showAsyncPrompt">Prompt With Async</VbenButton>
</div>
</template>

View File

@ -198,9 +198,16 @@ class PreferenceManager {
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', ({ matches: isDark }) => {
this.updatePreferences({
theme: { mode: isDark ? 'dark' : 'light' },
});
// 如果偏好设置中主题模式为auto则跟随系统更新
if (this.state.theme.mode === 'auto') {
this.updatePreferences({
theme: { mode: isDark ? 'dark' : 'light' },
});
// 恢复为auto模式
this.updatePreferences({
theme: { mode: 'auto' },
});
}
});
}

View File

@ -23,7 +23,6 @@ import {
import { useNamespace } from '@vben-core/composables';
import { Ellipsis } from '@vben-core/icons';
import { isHttpUrl } from '@vben-core/shared/utils';
import { useResizeObserver } from '@vueuse/core';
@ -248,9 +247,6 @@ function handleMenuItemClick(data: MenuItemClicked) {
if (!path || !parentPaths) {
return;
}
if (!isHttpUrl(path)) {
activePath.value = path;
}
emit('select', path, parentPaths);
}

View File

@ -208,6 +208,8 @@ onBeforeUnmount(() => {
nsMenu.e('popup-container'),
is(rootMenu.theme, true),
opened ? '' : 'hidden',
'overflow-auto',
'max-h-[calc(var(--radix-hover-card-content-available-height)-20px)]',
]"
:content-props="contentProps"
:open="true"

View File

@ -1,10 +1,10 @@
import type { Component } from 'vue';
import type { Component, VNode } from 'vue';
import type { Recordable } from '@vben-core/typings';
import type { AlertProps, BeforeCloseScope } from './alert';
import type { AlertProps, BeforeCloseScope, PromptProps } from './alert';
import { h, ref, render } from 'vue';
import { h, nextTick, ref, render } from 'vue';
import { useSimpleLocale } from '@vben-core/composables';
import { Input } from '@vben-core/shadcn-ui';
@ -130,40 +130,58 @@ export function vbenConfirm(
}
export async function vbenPrompt<T = any>(
options: Omit<AlertProps, 'beforeClose'> & {
beforeClose?: (scope: {
isConfirm: boolean;
value: T | undefined;
}) => boolean | Promise<boolean | undefined> | undefined;
component?: Component;
componentProps?: Recordable<any>;
defaultValue?: T;
modelPropName?: string;
},
options: PromptProps<T>,
): Promise<T | undefined> {
const {
component: _component,
componentProps: _componentProps,
componentSlots,
content,
defaultValue,
modelPropName: _modelPropName,
...delegated
} = options;
const contents: Component[] = [];
const modelValue = ref<T | undefined>(defaultValue);
const inputComponentRef = ref<null | VNode>(null);
const staticContents: Component[] = [];
if (isString(content)) {
contents.push(h('span', content));
} else {
contents.push(content);
staticContents.push(h('span', content));
} else if (content) {
staticContents.push(content as Component);
}
const componentProps = _componentProps || {};
const modelPropName = _modelPropName || 'modelValue';
componentProps[modelPropName] = modelValue.value;
componentProps[`onUpdate:${modelPropName}`] = (val: any) => {
modelValue.value = val;
const componentProps = { ..._componentProps };
// 每次渲染时都会重新计算的内容函数
const contentRenderer = () => {
const currentProps = { ...componentProps };
// 设置当前值
currentProps[modelPropName] = modelValue.value;
// 设置更新处理函数
currentProps[`onUpdate:${modelPropName}`] = (val: T) => {
modelValue.value = val;
};
// 创建输入组件
inputComponentRef.value = h(
_component || Input,
currentProps,
componentSlots,
);
// 返回包含静态内容和输入组件的数组
return h(
'div',
{ class: 'flex flex-col gap-2' },
{ default: () => [...staticContents, inputComponentRef.value] },
);
};
const componentRef = h(_component || Input, componentProps);
contents.push(componentRef);
const props: AlertProps & Recordable<any> = {
...delegated,
async beforeClose(scope: BeforeCloseScope) {
@ -174,23 +192,46 @@ export async function vbenPrompt<T = any>(
});
}
},
content: h(
'div',
{ class: 'flex flex-col gap-2' },
{ default: () => contents },
),
onOpened() {
// 组件挂载完成后,自动聚焦到输入组件
if (
componentRef.component?.exposed &&
isFunction(componentRef.component.exposed.focus)
) {
componentRef.component.exposed.focus();
} else if (componentRef.el && isFunction(componentRef.el.focus)) {
componentRef.el.focus();
// 使用函数形式,每次渲染都会重新计算内容
content: contentRenderer,
contentMasking: true,
async onOpened() {
await nextTick();
const componentRef: null | VNode = inputComponentRef.value;
if (componentRef) {
if (
componentRef.component?.exposed &&
isFunction(componentRef.component.exposed.focus)
) {
componentRef.component.exposed.focus();
} else {
if (componentRef.el) {
if (
isFunction(componentRef.el.focus) &&
['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA'].includes(
componentRef.el.tagName,
)
) {
componentRef.el.focus();
} else if (isFunction(componentRef.el.querySelector)) {
const focusableElement = componentRef.el.querySelector(
'input, select, textarea, button',
);
if (focusableElement && isFunction(focusableElement.focus)) {
focusableElement.focus();
}
} else if (
componentRef.el.nextElementSibling &&
isFunction(componentRef.el.nextElementSibling.focus)
) {
componentRef.el.nextElementSibling.focus();
}
}
}
}
},
};
await vbenConfirm(props);
return modelValue.value;
}

View File

@ -1,4 +1,6 @@
import type { Component } from 'vue';
import type { Component, VNode, VNodeArrayChildren } from 'vue';
import type { Recordable } from '@vben-core/typings';
export type IconType = 'error' | 'info' | 'question' | 'success' | 'warning';
@ -13,6 +15,11 @@ export type AlertProps = {
) => boolean | Promise<boolean | undefined> | undefined;
/** 边框 */
bordered?: boolean;
/**
*
* @default 'end'
*/
buttonAlign?: 'center' | 'end' | 'start';
/** 取消按钮的标题 */
cancelText?: string;
/** 是否居中显示 */
@ -25,10 +32,41 @@ export type AlertProps = {
content: Component | string;
/** 弹窗内容的额外样式 */
contentClass?: string;
/** 执行beforeClose回调期间在内容区域显示一个loading遮罩*/
contentMasking?: boolean;
/** 弹窗底部内容(与按钮在同一个容器中) */
footer?: Component | string;
/** 弹窗的图标(在标题的前面) */
icon?: Component | IconType;
/**
*
*/
overlayBlur?: number;
/** 是否显示取消按钮 */
showCancel?: boolean;
/** 弹窗标题 */
title?: string;
};
/** Prompt属性 */
export type PromptProps<T = any> = {
/** 关闭前的回调如果返回false则终止关闭 */
beforeClose?: (scope: {
isConfirm: boolean;
value: T | undefined;
}) => boolean | Promise<boolean | undefined> | undefined;
/** 用于接受用户输入的组件 */
component?: Component;
/** 输入组件的属性 */
componentProps?: Recordable<any>;
/** 输入组件的插槽 */
componentSlots?:
| (() => any)
| Recordable<unknown>
| VNode
| VNodeArrayChildren;
/** 默认值 */
defaultValue?: T;
/** 输入组件的值属性名 */
modelPropName?: string;
} & Omit<AlertProps, 'beforeClose'>;

View File

@ -3,7 +3,7 @@ import type { Component } from 'vue';
import type { AlertProps } from './alert';
import { computed, h, nextTick, ref, watch } from 'vue';
import { computed, h, nextTick, ref } from 'vue';
import { useSimpleLocale } from '@vben-core/composables';
import {
@ -30,6 +30,7 @@ import { cn } from '@vben-core/shared/utils';
const props = withDefaults(defineProps<AlertProps>(), {
bordered: true,
buttonAlign: 'end',
centered: true,
containerClass: 'w-[520px]',
});
@ -38,14 +39,12 @@ const open = defineModel<boolean>('open', { default: false });
const { $t } = useSimpleLocale();
const components = globalShareState.getComponents();
const isConfirm = ref(false);
watch(open, async (val) => {
await nextTick();
if (val) {
isConfirm.value = false;
} else {
emits('closed', isConfirm.value);
}
});
function onAlertClosed() {
emits('closed', isConfirm.value);
isConfirm.value = false;
}
const getIconRender = computed(() => {
let iconRender: Component | null = null;
if (props.icon) {
@ -99,6 +98,7 @@ function handleCancel() {
const loading = ref(false);
async function handleOpenChange(val: boolean) {
await nextTick();
if (!val && props.beforeClose) {
loading.value = true;
try {
@ -119,15 +119,16 @@ async function handleOpenChange(val: boolean) {
<AlertDialogContent
:open="open"
:centered="centered"
:overlay-blur="overlayBlur"
@opened="emits('opened')"
@closed="onAlertClosed"
:class="
cn(
containerClass,
'left-0 right-0 top-[10vh] mx-auto flex max-h-[80%] flex-col p-0 duration-300 sm:rounded-[var(--radius)] md:w-[520px] md:max-w-[80%]',
'left-0 right-0 mx-auto flex max-h-[80%] flex-col p-0 duration-300 sm:rounded-[var(--radius)] md:w-[520px] md:max-w-[80%]',
{
'border-border border': bordered,
'shadow-3xl': !bordered,
'top-1/2 !-translate-y-1/2': centered,
},
)
"
@ -137,7 +138,7 @@ async function handleOpenChange(val: boolean) {
<div class="flex items-center">
<component :is="getIconRender" class="mr-2" />
<span class="flex-auto">{{ $t(title) }}</span>
<AlertDialogCancel v-if="showCancel">
<AlertDialogCancel v-if="showCancel" as-child>
<VbenButton
variant="ghost"
size="icon"
@ -154,19 +155,24 @@ async function handleOpenChange(val: boolean) {
<div class="m-4 mb-6 min-h-[30px]">
<VbenRenderContent :content="content" render-br />
</div>
<VbenLoading v-if="loading" :spinning="loading" />
<VbenLoading v-if="loading && contentMasking" :spinning="loading" />
</AlertDialogDescription>
<div class="flex justify-end gap-x-2">
<AlertDialogCancel v-if="showCancel" :disabled="loading">
<div
class="flex items-center justify-end gap-x-2"
:class="`justify-${buttonAlign}`"
>
<VbenRenderContent :content="footer" />
<AlertDialogCancel v-if="showCancel" as-child>
<component
:is="components.DefaultButton || VbenButton"
:disabled="loading"
variant="ghost"
@click="handleCancel"
>
{{ cancelText || $t('cancel') }}
</component>
</AlertDialogCancel>
<AlertDialogAction>
<AlertDialogAction as-child>
<component
:is="components.PrimaryButton || VbenButton"
:loading="loading"

View File

@ -44,6 +44,7 @@ export class ModalApi {
confirmDisabled: false,
confirmLoading: false,
contentClass: '',
destroyOnClose: true,
draggable: false,
footer: true,
footerClass: '',

View File

@ -60,6 +60,10 @@ export interface ModalProps {
*
*/
description?: string;
/**
*
*/
destroyOnClose?: boolean;
/**
*
* @default false
@ -153,10 +157,6 @@ export interface ModalApiOptions extends ModalState {
*
*/
connectedComponent?: Component;
/**
* 使 connectedComponent
*/
destroyOnClose?: boolean;
/**
* false
* @returns

View File

@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { ExtendedModalApi, ModalProps } from './modal';
import { computed, nextTick, provide, ref, useId, watch } from 'vue';
import { computed, nextTick, provide, ref, unref, useId, watch } from 'vue';
import {
useIsMobile,
@ -34,6 +34,7 @@ interface Props extends ModalProps {
const props = withDefaults(defineProps<Props>(), {
appendToMain: false,
destroyOnClose: true,
modalApi: undefined,
});
@ -67,6 +68,7 @@ const {
confirmText,
contentClass,
description,
destroyOnClose,
draggable,
footer: showFooter,
footerClass,
@ -100,10 +102,15 @@ const { dragging, transform } = useModalDraggable(
shouldDraggable,
);
const firstOpened = ref(false);
const isClosed = ref(false);
watch(
() => state?.value?.isOpen,
async (v) => {
if (v) {
isClosed.value = false;
if (!firstOpened.value) firstOpened.value = true;
await nextTick();
if (!contentRef.value) return;
const innerContentRef = contentRef.value.getContentRef();
@ -113,6 +120,7 @@ watch(
dialogRef.value.style.transform = `translate(${offsetX}px, ${offsetY}px)`;
}
},
{ immediate: true },
);
watch(
@ -176,6 +184,15 @@ const getAppendTo = computed(() => {
? `#${ELEMENT_ID_MAIN_CONTENT}>div:not(.absolute)>div`
: undefined;
});
const getForceMount = computed(() => {
return !unref(destroyOnClose);
});
function handleClosed() {
isClosed.value = true;
props.modalApi?.onClosed();
}
</script>
<template>
<Dialog
@ -197,9 +214,11 @@ const getAppendTo = computed(() => {
shouldFullscreen,
'top-1/2 !-translate-y-1/2': centered && !shouldFullscreen,
'duration-300': !dragging,
hidden: isClosed,
},
)
"
:force-mount="getForceMount"
:modal="modal"
:open="state?.isOpen"
:show-close="closable"
@ -207,7 +226,7 @@ const getAppendTo = computed(() => {
:overlay-blur="overlayBlur"
close-class="top-3"
@close-auto-focus="handleFocusOutside"
@closed="() => modalApi?.onClosed()"
@closed="handleClosed"
:close-disabled="submitting"
@escape-key-down="escapeKeyDown"
@focus-outside="handleFocusOutside"

View File

@ -1,14 +1,6 @@
import type { ExtendedModalApi, ModalApiOptions, ModalProps } from './modal';
import {
defineComponent,
h,
inject,
nextTick,
provide,
reactive,
ref,
} from 'vue';
import { defineComponent, h, inject, nextTick, provide, reactive } from 'vue';
import { useStore } from '@vben-core/shared/store';
@ -32,7 +24,6 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
const { connectedComponent } = options;
if (connectedComponent) {
const extendedApi = reactive({});
const isModalReady = ref(true);
const Modal = defineComponent(
(props: TParentModalProps, { attrs, slots }) => {
provide(USER_MODAL_INJECT_KEY, {
@ -42,11 +33,6 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
Object.setPrototypeOf(extendedApi, api);
},
options,
async reCreateModal() {
isModalReady.value = false;
await nextTick();
isModalReady.value = true;
},
});
checkProps(extendedApi as ExtendedModalApi, {
...props,
@ -55,7 +41,7 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
});
return () =>
h(
isModalReady.value ? connectedComponent : 'div',
connectedComponent,
{
...props,
...attrs,
@ -84,14 +70,6 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
injectData.options?.onOpenChange?.(isOpen);
};
const onClosed = mergedOptions.onClosed;
mergedOptions.onClosed = () => {
onClosed?.();
if (mergedOptions.destroyOnClose) {
injectData.reCreateModal?.();
}
};
const api = new ModalApi(mergedOptions);
const extendedApi: ExtendedModalApi = api as never;

View File

@ -6,11 +6,11 @@ import type { ValueType, VbenButtonGroupProps } from './button';
import { computed, ref, watch } from 'vue';
import { Circle, CircleCheckBig, LoaderCircle } from '@vben-core/icons';
import { VbenRenderContent } from '@vben-core/shadcn-ui';
import { cn, isFunction } from '@vben-core/shared/utils';
import { objectOmit } from '@vueuse/core';
import { VbenRenderContent } from '../render-content';
import VbenButtonGroup from './button-group.vue';
import Button from './button.vue';

View File

@ -55,12 +55,13 @@ withDefaults(defineProps<Props>(), {
:size="logoSize"
class="relative rounded-none bg-transparent"
/>
<span
v-if="!collapsed"
class="text-foreground truncate text-nowrap font-semibold"
>
{{ text }}
</span>
<template v-if="!collapsed">
<slot name="text">
<span class="text-foreground truncate text-nowrap font-semibold">
{{ text }}
</span>
</slot>
</template>
</a>
</div>
</template>

View File

@ -61,7 +61,7 @@ defineExpose({
<template>
<AlertDialogPortal>
<Transition name="fade">
<Transition name="fade" appear>
<AlertDialogOverlay
v-if="open && modal"
:style="{
@ -80,7 +80,17 @@ defineExpose({
v-bind="forwarded"
:class="
cn(
'z-popup 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',
'z-popup bg-background w-full p-6 shadow-lg outline-none sm:rounded-xl',
'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
{
'data-[state=open]:slide-in-from-top-[48%] data-[state=closed]:slide-out-to-top-[48%]':
!centered,
'data-[state=open]:slide-in-from-top-[98%] data-[state=closed]:slide-out-to-top-[148%]':
centered,
'top-[10vh]': !centered,
'top-1/2 -translate-y-1/2': centered,
},
props.class,
)
"

View File

@ -17,6 +17,14 @@
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
},
"./es/tippy": {
"types": "./src/components/tippy/index.ts",
"default": "./src/components/tippy/index.ts"
},
"./es/loading": {
"types": "./src/components/loading/index.ts",
"default": "./src/components/loading/index.ts"
}
},
"dependencies": {

View File

@ -54,6 +54,20 @@ interface Props {
visibleEvent?: string;
/** 组件的v-model属性名默认为modelValue。部分组件可能为value */
modelPropName?: string;
/**
* 自动选择
* - `first`自动选择第一个选项
* - `last`自动选择最后一个选项
* - `one`: 当请求的结果只有一个选项时自动选择该选项
* - 函数自定义选择逻辑函数的参数为请求的结果数组返回值为选择的选项
* - false不自动选择(默认)
*/
autoSelect?:
| 'first'
| 'last'
| 'one'
| ((item: OptionsItem[]) => OptionsItem)
| false;
}
defineOptions({ name: 'ApiComponent', inheritAttrs: false });
@ -74,6 +88,7 @@ const props = withDefaults(defineProps<Props>(), {
afterFetch: undefined,
modelPropName: 'modelValue',
api: undefined,
autoSelect: false,
options: () => [],
});
@ -81,7 +96,7 @@ const emit = defineEmits<{
optionsChange: [OptionsItem[]];
}>();
const modelValue = defineModel({ default: '' });
const modelValue = defineModel<any>({ default: undefined });
const attrs = useAttrs();
const innerParams = ref({});
@ -194,6 +209,35 @@ watch(
);
function emitChange() {
if (
modelValue.value === undefined &&
props.autoSelect &&
unref(getOptions).length > 0
) {
let firstOption;
if (isFunction(props.autoSelect)) {
firstOption = props.autoSelect(unref(getOptions));
} else {
switch (props.autoSelect) {
case 'first': {
firstOption = unref(getOptions)[0];
break;
}
case 'last': {
firstOption = unref(getOptions)[unref(getOptions).length - 1];
break;
}
case 'one': {
if (unref(getOptions).length === 1) {
firstOption = unref(getOptions)[0];
}
break;
}
}
}
if (firstOption) modelValue.value = firstOption.value;
}
emit('optionsChange', unref(getOptions));
}
const componentRef = ref();

View File

@ -1,9 +1,8 @@
<script lang="ts" setup>
import type { ComponentInternalInstance } from 'vue';
import type { VerificationProps } from '../types';
import {
type ComponentInternalInstance,
getCurrentInstance,
nextTick,
onMounted,
@ -21,6 +20,44 @@ import { resetSize } from '../utils/util';
* VerifyPoints
* @description 点选
*/
// const props = defineProps({
// barSize: {
// default() {
// return {
// height: '40px',
// width: '310px',
// };
// },
// type: Object,
// },
// captchaType: {
// default() {
// return 'VerifyPoints';
// },
// type: String,
// },
// imgSize: {
// default() {
// return {
// height: '155px',
// width: '310px',
// };
// },
// type: Object,
// },
// // popfixed
// mode: {
// default: 'fixed',
// type: String,
// },
// //
// vSpace: {
// default: 5,
// type: Number,
// },
// });
defineOptions({
name: 'VerifyPoints',
});
@ -90,7 +127,7 @@ onMounted(() => {
const canvas = ref(null);
//
const getMousePos = function (_obj: any, e: any) {
const getMousePos = function (obj: any, e: any) {
const x = e.offsetX;
const y = e.offsetY;
return { x, y };
@ -153,7 +190,7 @@ function canvasClick(e: any) {
if (res.repCode === '0000') {
barAreaColor.value = '#4cae4c';
barAreaBorderColor.value = '#5cb85c';
text.value = $t('ui.captcha.sliderSuccessText');
text.value = $t('ui.captcha.success');
bindingClick.value = false;
if (mode.value === 'pop') {
setTimeout(() => {
@ -190,7 +227,7 @@ async function getPictrue() {
backToken.value = res.data.repData.token;
secretKey.value = res.data.repData.secretKey;
poinTextList.value = res.data.repData.wordList;
text.value = `${$t('ui.captcha.clickInOrder')}${poinTextList.value.join(',')}`;
text.value = `${$t('ui.captcha.point')}${poinTextList.value.join(',')}`;
} else {
text.value = res?.data?.repMsg;
}

View File

@ -14,12 +14,15 @@ export * from '@vben-core/popup-ui';
// 给文档用
export {
VbenAvatar,
VbenButton,
VbenButtonGroup,
VbenCheckButtonGroup,
VbenCountToAnimator,
VbenFullScreen,
VbenInputPassword,
VbenLoading,
VbenLogo,
VbenPinInput,
VbenSpinner,
VbenTree,

View File

@ -5,6 +5,7 @@ import type {
RouteLocationNormalizedLoadedGeneric,
} from 'vue-router';
import { computed } from 'vue';
import { RouterView } from 'vue-router';
import { preferences, usePreferences } from '@vben/preferences';
@ -20,6 +21,15 @@ const { keepAlive } = usePreferences();
const { getCachedTabs, getExcludeCachedTabs, renderRouteView } =
storeToRefs(tabbarStore);
/**
* 是否使用动画
*/
const getEnabledTransition = computed(() => {
const { transition } = preferences;
const transitionName = transition.name;
return transitionName && transition.enable;
});
//
function getTransitionName(_route: RouteLocationNormalizedLoaded) {
// 使
@ -90,7 +100,12 @@ function transformComponent(
<div class="relative h-full">
<IFrameRouterView />
<RouterView v-slot="{ Component, route }">
<Transition :name="getTransitionName(route)" appear mode="out-in">
<Transition
v-if="getEnabledTransition"
:name="getTransitionName(route)"
appear
mode="out-in"
>
<KeepAlive
v-if="keepAlive"
:exclude="getExcludeCachedTabs"
@ -109,6 +124,25 @@ function transformComponent(
:key="route.fullPath"
/>
</Transition>
<template v-else>
<KeepAlive
v-if="keepAlive"
:exclude="getExcludeCachedTabs"
:include="getCachedTabs"
>
<component
:is="transformComponent(Component, route)"
v-if="renderRouteView"
v-show="!route.meta.iframeSrc"
:key="route.fullPath"
/>
</KeepAlive>
<component
:is="Component"
v-else-if="renderRouteView"
:key="route.fullPath"
/>
</template>
</RouterView>
</div>
</template>

View File

@ -228,7 +228,11 @@ const headerSlots = computed(() => {
:text="preferences.app.name"
:theme="showHeaderNav ? headerTheme : theme"
@click="clickLogo"
/>
>
<template v-if="$slots['logo-text']" #text>
<slot name="logo-text"></slot>
</template>
</VbenLogo>
</template>
<!-- 头部区域 -->
<template #header>
@ -310,7 +314,11 @@ const headerSlots = computed(() => {
v-if="preferences.logo.enable"
:text="preferences.app.name"
:theme="theme"
/>
>
<template v-if="$slots['logo-text']" #text>
<slot name="logo-text"></slot>
</template>
</VbenLogo>
</template>
<template #tabbar>

View File

@ -13,7 +13,7 @@ import { useNavigation } from './use-navigation';
function useExtraMenu(useRootMenus?: ComputedRef<MenuRecordRaw[]>) {
const accessStore = useAccessStore();
const { navigation } = useNavigation();
const { navigation, willOpenedByWindow } = useNavigation();
const menus = computed(() => useRootMenus?.value ?? accessStore.accessMenus);
@ -33,11 +33,15 @@ function useExtraMenu(useRootMenus?: ComputedRef<MenuRecordRaw[]>) {
* @param menu
*/
const handleMixedMenuSelect = async (menu: MenuRecordRaw) => {
extraMenus.value = menu?.children ?? [];
extraActiveMenu.value = menu.parents?.[parentLevel.value] ?? menu.path;
const hasChildren = extraMenus.value.length > 0;
const _extraMenus = menu?.children ?? [];
const hasChildren = _extraMenus.length > 0;
if (!willOpenedByWindow(menu.path)) {
extraMenus.value = _extraMenus ?? [];
extraActiveMenu.value = menu.parents?.[parentLevel.value] ?? menu.path;
sidebarExtraVisible.value = hasChildren;
}
sidebarExtraVisible.value = hasChildren;
if (!hasChildren) {
await navigation(menu.path);
} else if (preferences.sidebar.autoActivateChild) {

View File

@ -10,7 +10,7 @@ import { findRootMenuByPath } from '@vben/utils';
import { useNavigation } from './use-navigation';
function useMixedMenu() {
const { navigation } = useNavigation();
const { navigation, willOpenedByWindow } = useNavigation();
const accessStore = useAccessStore();
const route = useRoute();
const splitSideMenus = ref<MenuRecordRaw[]>([]);
@ -89,11 +89,15 @@ function useMixedMenu() {
navigation(key);
return;
}
const rootMenu = menus.value.find((item) => item.path === key);
rootMenuPath.value = rootMenu?.path ?? '';
splitSideMenus.value = rootMenu?.children ?? [];
if (splitSideMenus.value.length === 0) {
const _splitSideMenus = rootMenu?.children ?? [];
if (!willOpenedByWindow(key)) {
rootMenuPath.value = rootMenu?.path ?? '';
splitSideMenus.value = _splitSideMenus;
}
if (_splitSideMenus.length === 0) {
navigation(key);
} else if (rootMenu && preferences.sidebar.autoActivateChild) {
navigation(

View File

@ -29,7 +29,19 @@ function useNavigation() {
}
};
return { navigation };
const willOpenedByWindow = (path: string) => {
const route = routeMetaMap.get(path);
const { openInNewWindow = false } = route?.meta ?? {};
if (isHttpUrl(path)) {
return true;
} else if (openInNewWindow) {
return true;
} else {
return false;
}
};
return { navigation, willOpenedByWindow };
}
export { useNavigation };

View File

@ -9,6 +9,8 @@ import { $t } from '@vben/locales';
import { BUILT_IN_THEME_PRESETS } from '@vben/preferences';
import { convertToHsl, TinyColor } from '@vben/utils';
import { useThrottleFn } from '@vueuse/core';
defineOptions({
name: 'PreferenceBuiltinTheme',
});
@ -19,6 +21,15 @@ const colorInput = ref();
const modelValue = defineModel<BuiltinThemeType>({ default: 'default' });
const themeColorPrimary = defineModel<string>('themeColorPrimary');
const updateThemeColorPrimary = useThrottleFn(
(value: string) => {
themeColorPrimary.value = value;
},
300,
true,
true,
);
const inputValue = computed(() => {
return new TinyColor(themeColorPrimary.value || '').toHexString();
});
@ -84,7 +95,7 @@ function handleSelect(theme: BuiltinThemePreset) {
function handleInputChange(e: Event) {
const target = e.target as HTMLInputElement;
themeColorPrimary.value = convertToHsl(target.value);
updateThemeColorPrimary(convertToHsl(target.value));
}
function selectColor() {

View File

@ -106,7 +106,7 @@ export function setupVbenVxeTable(setupOptions: SetupVxeTable) {
initVxeTable();
useTableForm = useVbenForm;
const preference = usePreferences();
const { isDark, locale } = usePreferences();
const localMap = {
'zh-CN': zhCN,
@ -114,11 +114,11 @@ export function setupVbenVxeTable(setupOptions: SetupVxeTable) {
};
watch(
[() => preference.theme.value, () => preference.locale.value],
([theme, locale]) => {
VxeUI.setTheme(theme === 'dark' ? 'dark' : 'light');
VxeUI.setI18n(locale, localMap[locale]);
VxeUI.setLanguage(locale);
[() => isDark.value, () => locale.value],
([isDarkValue, localeValue]) => {
VxeUI.setTheme(isDarkValue ? 'dark' : 'light');
VxeUI.setI18n(localeValue, localMap[localeValue]);
VxeUI.setLanguage(localeValue);
},
{
immediate: true,

View File

@ -31,6 +31,10 @@ export interface VxeTableGridOptions<T = any> extends VxeTableGridProps<T> {
toolbarConfig?: ToolbarConfigOptions;
}
export interface SeparatorOptions {
show?: boolean;
backgroundColor?: string;
}
export interface VxeGridProps {
/**
*
@ -64,6 +68,10 @@ export interface VxeGridProps {
*
*/
showSearchForm?: boolean;
/**
*
*/
separator?: boolean | SeparatorOptions;
}
export type ExtendedVxeGridApi = VxeGridApi & {

View File

@ -29,7 +29,13 @@ import { usePriorityValues } from '@vben/hooks';
import { EmptyIcon } from '@vben/icons';
import { $t } from '@vben/locales';
import { usePreferences } from '@vben/preferences';
import { cloneDeep, cn, isEqual, mergeWithArrayOverride } from '@vben/utils';
import {
cloneDeep,
cn,
isBoolean,
isEqual,
mergeWithArrayOverride,
} from '@vben/utils';
import { VbenHelpTooltip, VbenLoading } from '@vben-core/shadcn-ui';
@ -67,10 +73,30 @@ const {
tableTitle,
tableTitleHelp,
showSearchForm,
separator,
} = usePriorityValues(props, state);
const { isMobile } = usePreferences();
const isSeparator = computed(() => {
if (
!formOptions.value ||
showSearchForm.value === false ||
separator.value === false
) {
return false;
}
if (separator.value === true || separator.value === undefined) {
return true;
}
return separator.value.show !== false;
});
const separatorBg = computed(() => {
return !separator.value ||
isBoolean(separator.value) ||
!separator.value.backgroundColor
? undefined
: separator.value.backgroundColor;
});
const slots: SetupContext['slots'] = useSlots();
const [Form, formApi] = useTableForm({
@ -375,7 +401,18 @@ onUnmounted(() => {
<div
v-if="formOptions"
v-show="showSearchForm !== false"
:class="cn('relative rounded py-3', isCompactForm ? 'pb-8' : 'pb-4')"
:class="
cn(
'relative rounded py-3',
isCompactForm
? isSeparator
? 'pb-8'
: 'pb-4'
: isSeparator
? 'pb-4'
: 'pb-0',
)
"
>
<slot name="form">
<Form>
@ -404,6 +441,10 @@ onUnmounted(() => {
</Form>
</slot>
<div
v-if="isSeparator"
:style="{
...(separatorBg ? { backgroundColor: separatorBg } : undefined),
}"
class="bg-background-deep z-100 absolute -left-2 bottom-1 h-2 w-[calc(100%+1rem)] overflow-hidden md:bottom-2 md:h-3"
></div>
</div>

View File

@ -8,35 +8,64 @@ import type { Component } from 'vue';
import type { BaseFormComponentType } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import { defineComponent, getCurrentInstance, h, ref } from 'vue';
import {
defineAsyncComponent,
defineComponent,
getCurrentInstance,
h,
ref,
} from 'vue';
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales';
import {
AutoComplete,
Button,
Checkbox,
CheckboxGroup,
DatePicker,
Divider,
Input,
InputNumber,
InputPassword,
Mentions,
notification,
Radio,
RadioGroup,
RangePicker,
Rate,
Select,
Space,
Switch,
Textarea,
TimePicker,
TreeSelect,
Upload,
} from 'ant-design-vue';
import { notification } from 'ant-design-vue';
const AutoComplete = defineAsyncComponent(
() => import('ant-design-vue/es/auto-complete'),
);
const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
const Checkbox = defineAsyncComponent(
() => import('ant-design-vue/es/checkbox'),
);
const CheckboxGroup = defineAsyncComponent(() =>
import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup),
);
const DatePicker = defineAsyncComponent(
() => import('ant-design-vue/es/date-picker'),
);
const Divider = defineAsyncComponent(() => import('ant-design-vue/es/divider'));
const Input = defineAsyncComponent(() => import('ant-design-vue/es/input'));
const InputNumber = defineAsyncComponent(
() => import('ant-design-vue/es/input-number'),
);
const InputPassword = defineAsyncComponent(() =>
import('ant-design-vue/es/input').then((res) => res.InputPassword),
);
const Mentions = defineAsyncComponent(
() => import('ant-design-vue/es/mentions'),
);
const Radio = defineAsyncComponent(() => import('ant-design-vue/es/radio'));
const RadioGroup = defineAsyncComponent(() =>
import('ant-design-vue/es/radio').then((res) => res.RadioGroup),
);
const RangePicker = defineAsyncComponent(() =>
import('ant-design-vue/es/date-picker').then((res) => res.RangePicker),
);
const Rate = defineAsyncComponent(() => import('ant-design-vue/es/rate'));
const Select = defineAsyncComponent(() => import('ant-design-vue/es/select'));
const Space = defineAsyncComponent(() => import('ant-design-vue/es/space'));
const Switch = defineAsyncComponent(() => import('ant-design-vue/es/switch'));
const Textarea = defineAsyncComponent(() =>
import('ant-design-vue/es/input').then((res) => res.Textarea),
);
const TimePicker = defineAsyncComponent(
() => import('ant-design-vue/es/time-picker'),
);
const TreeSelect = defineAsyncComponent(
() => import('ant-design-vue/es/tree-select'),
);
const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));
const withDefaultPlaceholder = <T extends Component>(
component: T,

View File

@ -1,14 +1,12 @@
import { createApp, watchEffect } from 'vue';
import { registerAccessDirective } from '@vben/access';
import { initTippy, registerLoadingDirective } from '@vben/common-ui';
import { MotionPlugin } from '@vben/plugins/motion';
import { registerLoadingDirective } from '@vben/common-ui';
import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores';
import '@vben/styles';
import '@vben/styles/antd';
import { VueQueryPlugin } from '@tanstack/vue-query';
import { useTitle } from '@vueuse/core';
import { $t, setupI18n } from '#/locales';
@ -21,13 +19,13 @@ async function bootstrap(namespace: string) {
// 初始化组件适配器
await initComponentAdapter();
// // 设置弹窗的默认配置
// 设置弹窗的默认配置
// setDefaultModalProps({
// fullscreenButton: false,
// });
// // 设置抽屉的默认配置
// 设置抽屉的默认配置
// setDefaultDrawerProps({
// // zIndex: 1020,
// zIndex: 1020,
// });
const app = createApp(App);
@ -48,15 +46,18 @@ async function bootstrap(namespace: string) {
registerAccessDirective(app);
// 初始化 tippy
const { initTippy } = await import('@vben/common-ui/es/tippy');
initTippy(app);
// 配置路由及路由守卫
app.use(router);
// 配置@tanstack/vue-query
const { VueQueryPlugin } = await import('@tanstack/vue-query');
app.use(VueQueryPlugin);
// 配置Motion插件
const { MotionPlugin } = await import('@vben/plugins/motion');
app.use(MotionPlugin);
// 动态更新标题

View File

@ -2,10 +2,10 @@ import type { RouteRecordRaw } from 'vue-router';
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
import { AuthPageLayout, BasicLayout } from '#/layouts';
import { $t } from '#/locales';
import Login from '#/views/_core/authentication/login.vue';
const BasicLayout = () => import('#/layouts/basic.vue');
const AuthPageLayout = () => import('#/layouts/auth.vue');
/** 全局404页面 */
const fallbackNotFoundRoute: RouteRecordRaw = {
component: () => import('#/views/_core/fallback/not-found.vue'),
@ -50,7 +50,7 @@ const coreRoutes: RouteRecordRaw[] = [
{
name: 'Login',
path: 'login',
component: Login,
component: () => import('#/views/_core/authentication/login.vue'),
meta: {
title: $t('page.auth.login'),
},

View File

@ -74,6 +74,7 @@ const [BaseForm, baseFormApi] = useVbenForm({
},
//
api: getAllMenusApi,
autoSelect: 'first',
},
//
fieldName: 'api',

View File

@ -16,15 +16,18 @@ const [Modal, modalApi] = useVbenModal({
},
onOpenChange(isOpen) {
if (isOpen) {
handleUpdate(10);
handleUpdate();
}
},
});
function handleUpdate(len: number) {
function handleUpdate(len?: number) {
modalApi.setState({ confirmDisabled: true, loading: true });
setTimeout(() => {
list.value = Array.from({ length: len }, (_v, k) => k + 1);
list.value = Array.from(
{ length: len ?? Math.floor(Math.random() * 10) + 1 },
(_v, k) => k + 1,
);
modalApi.setState({ confirmDisabled: false, loading: false });
}, 2000);
}
@ -40,7 +43,7 @@ function handleUpdate(len: number) {
{{ item }}
</div>
<template #prepend-footer>
<Button type="link" @click="handleUpdate(6)"></Button>
<Button type="link" @click="handleUpdate()"></Button>
</template>
</Modal>
</template>

View File

@ -24,7 +24,7 @@ const value = ref();
title="基础弹窗示例"
title-tooltip="标题提示内容"
>
此弹窗指定在内容区域打开
<Input v-model="value" placeholder="KeepAlive测试" />
此弹窗指定在内容区域打开并且在关闭之后弹窗内容不会被销毁
<Input v-model:value="value" placeholder="KeepAlive测试" />
</Modal>
</template>

View File

@ -138,6 +138,7 @@ function openConfirm() {
}, 1000);
});
},
centered: false,
content: '这是一个确认弹窗',
icon: 'question',
})
@ -160,6 +161,7 @@ async function openPrompt() {
componentProps: { placeholder: '不能吃芝士...' },
content: '中午吃了什么?',
icon: 'question',
overlayBlur: 3,
})
.then((res) => {
message.success(`用户输入了:${res}`);
@ -196,7 +198,7 @@ async function openPrompt() {
</template>
</Card>
<Card class="w-[300px]" title="指定容器">
<Card class="w-[300px]" title="指定容器+关闭后不销毁">
<p>在内容区域打开弹窗的示例</p>
<template #actions>
<Button type="primary" @click="openInContentModal"></Button>
@ -261,6 +263,9 @@ async function openPrompt() {
</template>
</Card>
<Card class="w-[300px]" title="轻量提示弹窗">
<template #extra>
<DocButton path="/components/common-ui/vben-alert" />
</template>
<p>通过快捷方法创建动态提示弹窗适合一些轻量的提示和确认输入等</p>
<template #actions>
<Button type="primary" @click="openAlert">Alert</Button>

File diff suppressed because it is too large Load Diff

View File

@ -21,22 +21,22 @@ catalog:
'@commitlint/cli': ^19.8.0
'@commitlint/config-conventional': ^19.8.0
'@ctrl/tinycolor': ^4.1.0
'@eslint/js': ^9.23.0
'@eslint/js': ^9.24.0
'@faker-js/faker': ^9.6.0
'@iconify/json': ^2.2.323
'@iconify/json': ^2.2.324
'@iconify/tailwind': ^1.2.0
'@iconify/vue': ^4.3.0
'@intlify/core-base': ^11.1.2
'@intlify/core-base': ^11.1.3
'@intlify/unplugin-vue-i18n': ^6.0.5
'@jspm/generator': ^2.5.1
'@manypkg/get-packages': ^2.2.2
'@nolebase/vitepress-plugin-git-changelog': ^2.15.1
'@nolebase/vitepress-plugin-git-changelog': ^2.16.0
'@playwright/test': ^1.51.1
'@pnpm/workspace.read-manifest': ^1000.1.2
'@pnpm/workspace.read-manifest': ^1000.1.3
'@stylistic/stylelint-plugin': ^3.1.2
'@tailwindcss/nesting': 0.0.0-insiders.565cd3e
'@tailwindcss/typography': ^0.5.16
'@tanstack/vue-query': ^5.71.1
'@tanstack/vue-query': ^5.72.0
'@tanstack/vue-store': ^0.7.0
'@types/archiver': ^6.0.3
'@types/eslint': ^9.6.1
@ -46,15 +46,15 @@ catalog:
'@types/lodash.get': ^4.4.9
'@types/lodash.isequal': ^4.5.8
'@types/lodash.set': ^4.3.9
'@types/node': ^22.13.17
'@types/node': ^22.14.0
'@types/nprogress': ^0.2.3
'@types/postcss-import': ^14.0.3
'@types/qrcode': ^1.5.5
'@types/qs': ^6.9.18
'@types/sortablejs': ^1.15.8
'@types/crypto-js': ^4.2.2
'@typescript-eslint/eslint-plugin': ^8.29.0
'@typescript-eslint/parser': ^8.29.0
'@typescript-eslint/eslint-plugin': ^8.29.1
'@typescript-eslint/parser': ^8.29.1
'@vee-validate/zod': ^4.15.0
'@vite-pwa/vitepress': ^0.5.4
'@vitejs/plugin-vue': ^5.2.3
@ -90,17 +90,17 @@ catalog:
dotenv: ^16.4.7
echarts: ^5.6.0
element-plus: ^2.9.7
eslint: ^9.23.0
eslint-config-turbo: ^2.4.4
eslint: ^9.24.0
eslint-config-turbo: ^2.5.0
eslint-plugin-command: ^0.2.7
eslint-plugin-eslint-comments: ^3.2.0
eslint-plugin-import-x: ^4.10.0
eslint-plugin-import-x: ^4.10.2
eslint-plugin-jsdoc: ^50.6.9
eslint-plugin-jsonc: ^2.20.0
eslint-plugin-n: ^17.17.0
eslint-plugin-no-only-tests: ^3.3.0
eslint-plugin-perfectionist: ^4.11.0
eslint-plugin-prettier: ^5.2.5
eslint-plugin-prettier: ^5.2.6
eslint-plugin-regexp: ^2.7.0
eslint-plugin-unicorn: ^56.0.1
eslint-plugin-unused-imports: ^4.1.4
@ -148,9 +148,9 @@ catalog:
rimraf: ^6.0.1
rollup: ^4.39.0
rollup-plugin-visualizer: ^5.14.0
sass: ^1.86.1
sass: ^1.86.3
sortablejs: ^1.15.6
stylelint: ^16.17.0
stylelint: ^16.18.0
stylelint-config-recess-order: ^5.1.1
stylelint-config-recommended: ^14.0.1
stylelint-config-recommended-scss: ^14.1.0
@ -164,12 +164,12 @@ catalog:
tailwindcss-animate: ^1.0.7
theme-colors: ^0.1.0
tippy.js: ^6.2.5
turbo: ^2.4.4
typescript: ^5.8.2
turbo: ^2.5.0
typescript: ^5.8.3
unbuild: ^3.5.0
unplugin-element-plus: ^0.9.1
vee-validate: ^4.15.0
vite: ^6.2.4
vite: ^6.2.5
vite-plugin-compression: ^0.5.1
vite-plugin-dts: ^4.5.3
vite-plugin-html: ^3.2.2
@ -181,12 +181,12 @@ catalog:
vitest: ^2.1.9
vue: ^3.5.13
vue-eslint-parser: ^9.4.3
vue-i18n: ^11.1.2
vue-i18n: ^11.1.3
vue-json-viewer: ^3.0.4
vue-router: ^4.5.0
vue-tippy: ^6.7.0
vue-tsc: 2.1.10
vxe-pc-ui: ^4.5.11
vxe-pc-ui: ^4.5.14
vxe-table: ^4.12.5
watermark-js-plus: ^1.5.8
zod: ^3.24.2