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

Merge pull request !73 from 芋道源码/vben-sync
This commit is contained in:
芋道源码
2025-04-14 11:43:00 +00:00
committed by Gitee
55 changed files with 2476 additions and 1334 deletions

13
.vscode/settings.json vendored
View File

@@ -223,5 +223,16 @@
"commentTranslate.multiLineMerge": true, "commentTranslate.multiLineMerge": true,
"vue.server.hybridMode": true, "vue.server.hybridMode": true,
"typescript.tsdk": "node_modules/typescript/lib", "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 { BaseFormComponentType } from '@vben/common-ui';
import type { Recordable } from '@vben/types'; 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 { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { import { notification } from 'ant-design-vue';
AutoComplete,
Button, const AutoComplete = defineAsyncComponent(
Checkbox, () => import('ant-design-vue/es/auto-complete'),
CheckboxGroup, );
DatePicker, const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
Divider, const Checkbox = defineAsyncComponent(
Input, () => import('ant-design-vue/es/checkbox'),
InputNumber, );
InputPassword, const CheckboxGroup = defineAsyncComponent(() =>
Mentions, import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup),
notification, );
Radio, const DatePicker = defineAsyncComponent(
RadioGroup, () => import('ant-design-vue/es/date-picker'),
RangePicker, );
Rate, const Divider = defineAsyncComponent(() => import('ant-design-vue/es/divider'));
Select, const Input = defineAsyncComponent(() => import('ant-design-vue/es/input'));
Space, const InputNumber = defineAsyncComponent(
Switch, () => import('ant-design-vue/es/input-number'),
Textarea, );
TimePicker, const InputPassword = defineAsyncComponent(() =>
TreeSelect, import('ant-design-vue/es/input').then((res) => res.InputPassword),
Upload, );
} from 'ant-design-vue'; 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>( const withDefaultPlaceholder = <T extends Component>(
component: T, component: T,

View File

@@ -1,8 +1,7 @@
import { createApp, watchEffect } from 'vue'; import { createApp, watchEffect } from 'vue';
import { registerAccessDirective } from '@vben/access'; import { registerAccessDirective } from '@vben/access';
import { initTippy, registerLoadingDirective } from '@vben/common-ui'; import { registerLoadingDirective } from '@vben/common-ui/es/loading';
import { MotionPlugin } from '@vben/plugins/motion';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores'; import { initStores } from '@vben/stores';
import '@vben/styles'; import '@vben/styles';
@@ -47,12 +46,14 @@ async function bootstrap(namespace: string) {
registerAccessDirective(app); registerAccessDirective(app);
// 初始化 tippy // 初始化 tippy
const { initTippy } = await import('@vben/common-ui/es/tippy');
initTippy(app); initTippy(app);
// 配置路由及路由守卫 // 配置路由及路由守卫
app.use(router); app.use(router);
// 配置Motion插件 // 配置Motion插件
const { MotionPlugin } = await import('@vben/plugins/motion');
app.use(MotionPlugin); 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 { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
import { AuthPageLayout, BasicLayout } from '#/layouts';
import { $t } from '#/locales'; 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页面 */ /** 全局404页面 */
const fallbackNotFoundRoute: RouteRecordRaw = { const fallbackNotFoundRoute: RouteRecordRaw = {
component: () => import('#/views/_core/fallback/not-found.vue'), component: () => import('#/views/_core/fallback/not-found.vue'),
@@ -50,7 +50,7 @@ const coreRoutes: RouteRecordRaw[] = [
{ {
name: 'Login', name: 'Login',
path: 'login', path: 'login',
component: Login, component: () => import('#/views/_core/authentication/login.vue'),
meta: { meta: {
title: $t('page.auth.login'), 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 { BaseFormComponentType } from '@vben/common-ui';
import type { Recordable } from '@vben/types'; 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 { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { import { ElNotification } from 'element-plus';
ElButton,
ElCheckbox, const ElButton = defineAsyncComponent(() =>
ElCheckboxButton, Promise.all([
ElCheckboxGroup, import('element-plus/es/components/button/index'),
ElDatePicker, import('element-plus/es/components/button/style/css'),
ElDivider, ]).then(([res]) => res.ElButton),
ElInput, );
ElInputNumber, const ElCheckbox = defineAsyncComponent(() =>
ElNotification, Promise.all([
ElRadio, import('element-plus/es/components/checkbox/index'),
ElRadioButton, import('element-plus/es/components/checkbox/style/css'),
ElRadioGroup, ]).then(([res]) => res.ElCheckbox),
ElSelectV2, );
ElSpace, const ElCheckboxButton = defineAsyncComponent(() =>
ElSwitch, Promise.all([
ElTimePicker, import('element-plus/es/components/checkbox/index'),
ElTreeSelect, import('element-plus/es/components/checkbox-button/style/css'),
ElUpload, ]).then(([res]) => res.ElCheckboxButton),
} from 'element-plus'; );
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>( const withDefaultPlaceholder = <T extends Component>(
component: T, component: T,

View File

@@ -1,8 +1,7 @@
import { createApp, watchEffect } from 'vue'; import { createApp, watchEffect } from 'vue';
import { registerAccessDirective } from '@vben/access'; import { registerAccessDirective } from '@vben/access';
import { initTippy, registerLoadingDirective } from '@vben/common-ui'; import { registerLoadingDirective } from '@vben/common-ui';
import { MotionPlugin } from '@vben/plugins/motion';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores'; import { initStores } from '@vben/stores';
import '@vben/styles'; import '@vben/styles';
@@ -49,12 +48,14 @@ async function bootstrap(namespace: string) {
registerAccessDirective(app); registerAccessDirective(app);
// 初始化 tippy // 初始化 tippy
const { initTippy } = await import('@vben/common-ui/es/tippy');
initTippy(app); initTippy(app);
// 配置路由及路由守卫 // 配置路由及路由守卫
app.use(router); app.use(router);
// 配置Motion插件 // 配置Motion插件
const { MotionPlugin } = await import('@vben/plugins/motion');
app.use(MotionPlugin); 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 { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
import { AuthPageLayout, BasicLayout } from '#/layouts';
import { $t } from '#/locales'; 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页面 */ /** 全局404页面 */
const fallbackNotFoundRoute: RouteRecordRaw = { const fallbackNotFoundRoute: RouteRecordRaw = {
component: () => import('#/views/_core/fallback/not-found.vue'), component: () => import('#/views/_core/fallback/not-found.vue'),
@@ -50,7 +50,7 @@ const coreRoutes: RouteRecordRaw[] = [
{ {
name: 'Login', name: 'Login',
path: 'login', path: 'login',
component: Login, component: () => import('#/views/_core/authentication/login.vue'),
meta: { meta: {
title: $t('page.auth.login'), title: $t('page.auth.login'),
}, },

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { h } from 'vue'; 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'; import { ElButton, ElCard, ElCheckbox, ElMessage } from 'element-plus';
@@ -17,11 +17,16 @@ const [Form, formApi] = useVbenForm({
}, },
layout: 'horizontal', layout: 'horizontal',
// 大屏一行显示3个中屏一行显示2个小屏一行显示1个 // 大屏一行显示3个中屏一行显示2个小屏一行显示1个
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) => { handleSubmit: (values) => {
ElMessage.success(`表单数据:${JSON.stringify(values)}`); ElMessage.success(`表单数据:${JSON.stringify(values)}`);
}, },
schema: [ schema: [
{
component: 'IconPicker',
fieldName: 'icon',
label: 'IconPicker',
},
{ {
// 组件需要在 #/adapter.ts内注册并加上类型 // 组件需要在 #/adapter.ts内注册并加上类型
component: 'ApiSelect', component: 'ApiSelect',
@@ -149,6 +154,8 @@ const [Form, formApi] = useVbenForm({
}, },
], ],
}); });
const [Drawer, drawerApi] = useVbenDrawer();
function setFormValues() { function setFormValues() {
formApi.setValues({ formApi.setValues({
string: 'string', string: 'string',
@@ -168,6 +175,9 @@ function setFormValues() {
description="我们重新包装了CheckboxGroup、RadioGroup、Select可以通过options属性传入选项属性数组以自动生成选项" description="我们重新包装了CheckboxGroup、RadioGroup、Select可以通过options属性传入选项属性数组以自动生成选项"
title="表单演示" title="表单演示"
> >
<Drawer class="w-[600px]" title="基础表单示例">
<Form />
</Drawer>
<ElCard> <ElCard>
<template #header> <template #header>
<div class="flex items-center"> <div class="flex items-center">
@@ -175,7 +185,7 @@ function setFormValues() {
<ElButton type="primary" @click="setFormValues">设置表单值</ElButton> <ElButton type="primary" @click="setFormValues">设置表单值</ElButton>
</div> </div>
</template> </template>
<Form /> <ElButton type="primary" @click="drawerApi.open"> 打开抽屉 </ElButton>
</ElCard> </ElCard>
</Page> </Page>
</template> </template>

View File

@@ -8,32 +8,68 @@ import type { Component } from 'vue';
import type { BaseFormComponentType } from '@vben/common-ui'; import type { BaseFormComponentType } from '@vben/common-ui';
import type { Recordable } from '@vben/types'; 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 { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales'; 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'; 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>( const withDefaultPlaceholder = <T extends Component>(
component: T, component: T,
type: 'input' | 'select', type: 'input' | 'select',

View File

@@ -1,8 +1,7 @@
import { createApp, watchEffect } from 'vue'; import { createApp, watchEffect } from 'vue';
import { registerAccessDirective } from '@vben/access'; import { registerAccessDirective } from '@vben/access';
import { initTippy, registerLoadingDirective } from '@vben/common-ui'; import { registerLoadingDirective } from '@vben/common-ui';
import { MotionPlugin } from '@vben/plugins/motion';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores'; import { initStores } from '@vben/stores';
import '@vben/styles'; import '@vben/styles';
@@ -47,12 +46,14 @@ async function bootstrap(namespace: string) {
registerAccessDirective(app); registerAccessDirective(app);
// 初始化 tippy // 初始化 tippy
const { initTippy } = await import('@vben/common-ui/es/tippy');
initTippy(app); initTippy(app);
// 配置路由及路由守卫 // 配置路由及路由守卫
app.use(router); app.use(router);
// 配置Motion插件 // 配置Motion插件
const { MotionPlugin } = await import('@vben/plugins/motion');
app.use(MotionPlugin); 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 { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
import { AuthPageLayout, BasicLayout } from '#/layouts';
import { $t } from '#/locales'; 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页面 */ /** 全局404页面 */
const fallbackNotFoundRoute: RouteRecordRaw = { const fallbackNotFoundRoute: RouteRecordRaw = {
component: () => import('#/views/_core/fallback/not-found.vue'), component: () => import('#/views/_core/fallback/not-found.vue'),
@@ -50,7 +50,7 @@ const coreRoutes: RouteRecordRaw[] = [
{ {
name: 'Login', name: 'Login',
path: 'login', path: 'login',
component: Login, component: () => import('#/views/_core/authentication/login.vue'),
meta: { meta: {
title: $t('page.auth.login'), 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-text: var(--vp-c-text-1);
--vp-custom-block-tip-bg: var(--vp-c-brand-soft); --vp-custom-block-tip-bg: var(--vp-c-brand-soft);
--vp-custom-block-tip-code-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) { @media (min-width: 640px) {

View File

@@ -43,6 +43,9 @@ export type BeforeCloseScope = {
isConfirm: boolean; isConfirm: boolean;
}; };
/**
* alert 属性
*/
export type AlertProps = { export type AlertProps = {
/** 关闭前的回调如果返回false则终止关闭 */ /** 关闭前的回调如果返回false则终止关闭 */
beforeClose?: ( beforeClose?: (
@@ -50,6 +53,8 @@ export type AlertProps = {
) => boolean | Promise<boolean | undefined> | undefined; ) => boolean | Promise<boolean | undefined> | undefined;
/** 边框 */ /** 边框 */
bordered?: boolean; bordered?: boolean;
/** 按钮对齐方式 */
buttonAlign?: 'center' | 'end' | 'start';
/** 取消按钮的标题 */ /** 取消按钮的标题 */
cancelText?: string; cancelText?: string;
/** 是否居中显示 */ /** 是否居中显示 */
@@ -62,14 +67,41 @@ export type AlertProps = {
content: Component | string; content: Component | string;
/** 弹窗内容的额外样式 */ /** 弹窗内容的额外样式 */
contentClass?: string; contentClass?: string;
/** 执行beforeClose回调期间在内容区域显示一个loading遮罩*/
contentMasking?: boolean;
/** 弹窗底部内容(与按钮在同一个容器中) */
footer?: Component | string;
/** 弹窗的图标(在标题的前面) */ /** 弹窗的图标(在标题的前面) */
icon?: Component | IconType; icon?: Component | IconType;
/**
* 弹窗遮罩模糊效果
*/
overlayBlur?: number;
/** 是否显示取消按钮 */ /** 是否显示取消按钮 */
showCancel?: boolean; showCancel?: boolean;
/** 弹窗标题 */ /** 弹窗标题 */
title?: string; 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的函数签名相同。 * alert和confirm的函数签名相同。

View File

@@ -131,26 +131,37 @@ function fetchApi(): Promise<Record<string, any>> {
### Props ### Props
| 属性名 | 描述 | 类型 | 默认值 | | 属性名 | 描述 | 类型 | 默认值 | 版本要求 |
| --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| modelValue(v-model) | 当前值 | `any` | - | | modelValue(v-model) | 当前值 | `any` | - | - |
| component | 欲包装的组件(以下称为目标组件) | `Component` | - | | component | 欲包装的组件(以下称为目标组件) | `Component` | - | - |
| numberToString | 是否将value从数字转为string | `boolean` | `false` | | numberToString | 是否将value从数字转为string | `boolean` | `false` | - |
| api | 获取数据的函数 | `(arg?: any) => Promise<OptionsItem[] \| Record<string, any>>` | - | | api | 获取数据的函数 | `(arg?: any) => Promise<OptionsItem[] \| Record<string, any>>` | - | - |
| params | 传递给api的参数 | `Record<string, any>` | - | | params | 传递给api的参数 | `Record<string, any>` | - | - |
| resultField | 从api返回的结果中提取options数组的字段名 | `string` | - | | resultField | 从api返回的结果中提取options数组的字段名 | `string` | - | - |
| labelField | label字段名 | `string` | `label` | | labelField | label字段名 | `string` | `label` | - |
| childrenField | 子级数据字段名,需要层级数据的组件可用 | `string` | `` | | childrenField | 子级数据字段名,需要层级数据的组件可用 | `string` | `` | - |
| valueField | value字段名 | `string` | `value` | | valueField | value字段名 | `string` | `value` | - |
| optionsPropName | 目标组件接收options数据的属性名称 | `string` | `options` | | optionsPropName | 目标组件接收options数据的属性名称 | `string` | `options` | - |
| modelPropName | 目标组件的双向绑定属性名默认为modelValue。部分组件可能为value | `string` | `modelValue` | | modelPropName | 目标组件的双向绑定属性名默认为modelValue。部分组件可能为value | `string` | `modelValue` | - |
| immediate | 是否立即调用api | `boolean` | `true` | | immediate | 是否立即调用api | `boolean` | `true` | - |
| alwaysLoad | 每次`visibleEvent`事件发生时都重新请求数据 | `boolean` | `false` | | alwaysLoad | 每次`visibleEvent`事件发生时都重新请求数据 | `boolean` | `false` | - |
| beforeFetch | 在api请求之前的回调函数 | `AnyPromiseFunction<any, any>` | - | | beforeFetch | 在api请求之前的回调函数 | `AnyPromiseFunction<any, any>` | - | - |
| afterFetch | 在api请求之后的回调函数 | `AnyPromiseFunction<any, any>` | - | | afterFetch | 在api请求之后的回调函数 | `AnyPromiseFunction<any, any>` | - | - |
| options | 直接传入选项数据也作为api返回空数据时的后备数据 | `OptionsItem[]` | - | | options | 直接传入选项数据也作为api返回空数据时的后备数据 | `OptionsItem[]` | - | - |
| visibleEvent | 触发重新请求数据的事件名 | `string` | - | | visibleEvent | 触发重新请求数据的事件名 | `string` | - | - |
| loadingSlot | 目标组件的插槽名称,用来显示一个"加载中"的图标 | `string` | - | | loadingSlot | 目标组件的插槽名称,用来显示一个"加载中"的图标 | `string` | - | - |
| autoSelect | 自动设置选项 | `'first' \| 'last' \| 'one'\| (item: OptionsItem[]) => OptionsItem \| false` | `false` | >5.5.4 |
#### autoSelect 自动设置选项
如果当前值为undefined在选项数据成功加载之后自动从备选项中选择一个作为当前值。默认值为`false`,即不自动选择选项。注意:该属性不应用于多选组件。可选值有:
- `first`:自动选择第一个选项
- `last`:自动选择最后一个选项
- `one`:有且仅有一个选项时,自动选择它
- `函数`自定义选择逻辑函数的参数为options返回值为选择的选项
- false不自动选择选项
### Methods ### Methods

View File

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

View File

@@ -167,6 +167,23 @@ vxeUI.renderer.add('CellLink', {
当启用了表单搜索时可以在toolbarConfig中配置`search``true`来让表格在工具栏区域显示一个搜索表单控制按钮。表格的所有以`form-`开头的命名插槽都会被传递给搜索表单。 当启用了表单搜索时可以在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" /> <DemoPreview dir="demos/vben-vxe-table/form" />
## 单元格编辑 ## 单元格编辑
@@ -231,15 +248,16 @@ useVbenVxeGrid 返回的第二个参数,是一个对象,包含了一些表
所有属性都可以传入 `useVbenVxeGrid` 的第一个参数中。 所有属性都可以传入 `useVbenVxeGrid` 的第一个参数中。
| 属性名 | 描述 | 类型 | | 属性名 | 描述 | 类型 | 版本要求 |
| -------------- | -------------------- | ------------------- | | --- | --- | --- | --- |
| tableTitle | 表格标题 | `string` | | tableTitle | 表格标题 | `string` | - |
| tableTitleHelp | 表格标题帮助信息 | `string` | | tableTitleHelp | 表格标题帮助信息 | `string` | - |
| gridClass | grid组件的class | `string` | | gridClass | grid组件的class | `string` | - |
| gridOptions | grid组件的参数 | `VxeTableGridProps` | | gridOptions | grid组件的参数 | `VxeTableGridProps` | - |
| gridEvents | grid组件的触发的事件 | `VxeGridListeners` | | gridEvents | grid组件的触发的事件 | `VxeGridListeners` | - |
| formOptions | 表单参数 | `VbenFormProps` | | formOptions | 表单参数 | `VbenFormProps` | - |
| showSearchForm | 是否显示搜索表单 | `boolean` | | showSearchForm | 是否显示搜索表单 | `boolean` | - |
| separator | 搜索表单与表格主体之间的分隔条 | `boolean\|SeparatorOptions` | >5.5.4 |
## Slots ## Slots

View File

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

View File

@@ -1,6 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { h, ref } from 'vue';
import { alert, confirm, VbenButton } from '@vben/common-ui'; import { alert, confirm, VbenButton } from '@vben/common-ui';
import { Checkbox, message } from 'ant-design-vue';
function showConfirm() { function showConfirm() {
confirm('This is an alert message') confirm('This is an alert message')
.then(() => { .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() { function showAsyncConfirm() {
confirm({ confirm({
beforeClose({ isConfirm }) { beforeClose({ isConfirm }) {
@@ -37,6 +69,7 @@ function showAsyncConfirm() {
<div class="flex gap-4"> <div class="flex gap-4">
<VbenButton @click="showConfirm">Confirm</VbenButton> <VbenButton @click="showConfirm">Confirm</VbenButton>
<VbenButton @click="showIconConfirm">Confirm With Icon</VbenButton> <VbenButton @click="showIconConfirm">Confirm With Icon</VbenButton>
<VbenButton @click="showfooterConfirm">Confirm With Footer</VbenButton>
<VbenButton @click="showAsyncConfirm">Async Confirm</VbenButton> <VbenButton @click="showAsyncConfirm">Async Confirm</VbenButton>
</div> </div>
</template> </template>

View File

@@ -1,7 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { h } from 'vue';
import { alert, prompt, VbenButton } from '@vben/common-ui'; 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() { function showPrompt() {
prompt({ 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() { function showSelectPrompt() {
prompt({ prompt({
component: VbenSelect, component: Select,
componentProps: { componentProps: {
options: [
{ label: 'Option A', value: 'Option A' },
{ label: 'Option B', value: 'Option B' },
{ label: 'Option C', value: 'Option C' },
],
placeholder: '请选择',
// 弹窗会设置body的pointer-events为none这回影响下拉框的点击事件
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: [ options: [
{ label: 'Option 1', value: 'option1' }, { label: 'Option 1', value: 'option1' },
{ label: 'Option 2', value: 'option2' }, { label: 'Option 2', value: 'option2' },
{ label: 'Option 3', value: 'option3' }, { label: 'Option 3', value: 'option3' },
], ],
placeholder: '请选择',
}, },
content: 'This is an alert message with icon', content: '选择一个选项后再点击[确认]',
icon: 'question', icon: 'question',
modelPropName: 'value',
}).then((val) => { }).then((val) => {
alert(`你选择的是${val}`); alert(`${val} 已设置。`);
}); });
} }
</script> </script>
<template> <template>
<div class="flex gap-4"> <div class="flex gap-4">
<VbenButton @click="showPrompt">Prompt</VbenButton> <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> </div>
</template> </template>

View File

@@ -198,9 +198,16 @@ class PreferenceManager {
window window
.matchMedia('(prefers-color-scheme: dark)') .matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', ({ matches: isDark }) => { .addEventListener('change', ({ matches: isDark }) => {
this.updatePreferences({ // 如果偏好设置中主题模式为auto则跟随系统更新
theme: { mode: isDark ? 'dark' : 'light' }, 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 { useNamespace } from '@vben-core/composables';
import { Ellipsis } from '@vben-core/icons'; import { Ellipsis } from '@vben-core/icons';
import { isHttpUrl } from '@vben-core/shared/utils';
import { useResizeObserver } from '@vueuse/core'; import { useResizeObserver } from '@vueuse/core';
@@ -248,9 +247,6 @@ function handleMenuItemClick(data: MenuItemClicked) {
if (!path || !parentPaths) { if (!path || !parentPaths) {
return; return;
} }
if (!isHttpUrl(path)) {
activePath.value = path;
}
emit('select', path, parentPaths); emit('select', path, parentPaths);
} }

View File

@@ -208,6 +208,8 @@ onBeforeUnmount(() => {
nsMenu.e('popup-container'), nsMenu.e('popup-container'),
is(rootMenu.theme, true), is(rootMenu.theme, true),
opened ? '' : 'hidden', opened ? '' : 'hidden',
'overflow-auto',
'max-h-[calc(var(--radix-hover-card-content-available-height)-20px)]',
]" ]"
:content-props="contentProps" :content-props="contentProps"
:open="true" :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 { 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 { useSimpleLocale } from '@vben-core/composables';
import { Input } from '@vben-core/shadcn-ui'; import { Input } from '@vben-core/shadcn-ui';
@@ -130,40 +130,58 @@ export function vbenConfirm(
} }
export async function vbenPrompt<T = any>( export async function vbenPrompt<T = any>(
options: Omit<AlertProps, 'beforeClose'> & { options: PromptProps<T>,
beforeClose?: (scope: {
isConfirm: boolean;
value: T | undefined;
}) => boolean | Promise<boolean | undefined> | undefined;
component?: Component;
componentProps?: Recordable<any>;
defaultValue?: T;
modelPropName?: string;
},
): Promise<T | undefined> { ): Promise<T | undefined> {
const { const {
component: _component, component: _component,
componentProps: _componentProps, componentProps: _componentProps,
componentSlots,
content, content,
defaultValue, defaultValue,
modelPropName: _modelPropName, modelPropName: _modelPropName,
...delegated ...delegated
} = options; } = options;
const contents: Component[] = [];
const modelValue = ref<T | undefined>(defaultValue); const modelValue = ref<T | undefined>(defaultValue);
const inputComponentRef = ref<null | VNode>(null);
const staticContents: Component[] = [];
if (isString(content)) { if (isString(content)) {
contents.push(h('span', content)); staticContents.push(h('span', content));
} else { } else if (content) {
contents.push(content); staticContents.push(content as Component);
} }
const componentProps = _componentProps || {};
const modelPropName = _modelPropName || 'modelValue'; const modelPropName = _modelPropName || 'modelValue';
componentProps[modelPropName] = modelValue.value; const componentProps = { ..._componentProps };
componentProps[`onUpdate:${modelPropName}`] = (val: any) => {
modelValue.value = val; // 每次渲染时都会重新计算的内容函数
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> = { const props: AlertProps & Recordable<any> = {
...delegated, ...delegated,
async beforeClose(scope: BeforeCloseScope) { async beforeClose(scope: BeforeCloseScope) {
@@ -174,23 +192,46 @@ export async function vbenPrompt<T = any>(
}); });
} }
}, },
content: h( // 使用函数形式,每次渲染都会重新计算内容
'div', content: contentRenderer,
{ class: 'flex flex-col gap-2' }, contentMasking: true,
{ default: () => contents }, async onOpened() {
), await nextTick();
onOpened() { const componentRef: null | VNode = inputComponentRef.value;
// 组件挂载完成后,自动聚焦到输入组件 if (componentRef) {
if ( if (
componentRef.component?.exposed && componentRef.component?.exposed &&
isFunction(componentRef.component.exposed.focus) isFunction(componentRef.component.exposed.focus)
) { ) {
componentRef.component.exposed.focus(); componentRef.component.exposed.focus();
} else if (componentRef.el && isFunction(componentRef.el.focus)) { } else {
componentRef.el.focus(); 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); await vbenConfirm(props);
return modelValue.value; 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'; export type IconType = 'error' | 'info' | 'question' | 'success' | 'warning';
@@ -13,6 +15,11 @@ export type AlertProps = {
) => boolean | Promise<boolean | undefined> | undefined; ) => boolean | Promise<boolean | undefined> | undefined;
/** 边框 */ /** 边框 */
bordered?: boolean; bordered?: boolean;
/**
* 按钮对齐方式
* @default 'end'
*/
buttonAlign?: 'center' | 'end' | 'start';
/** 取消按钮的标题 */ /** 取消按钮的标题 */
cancelText?: string; cancelText?: string;
/** 是否居中显示 */ /** 是否居中显示 */
@@ -25,10 +32,41 @@ export type AlertProps = {
content: Component | string; content: Component | string;
/** 弹窗内容的额外样式 */ /** 弹窗内容的额外样式 */
contentClass?: string; contentClass?: string;
/** 执行beforeClose回调期间在内容区域显示一个loading遮罩*/
contentMasking?: boolean;
/** 弹窗底部内容(与按钮在同一个容器中) */
footer?: Component | string;
/** 弹窗的图标(在标题的前面) */ /** 弹窗的图标(在标题的前面) */
icon?: Component | IconType; icon?: Component | IconType;
/**
* 弹窗遮罩模糊效果
*/
overlayBlur?: number;
/** 是否显示取消按钮 */ /** 是否显示取消按钮 */
showCancel?: boolean; showCancel?: boolean;
/** 弹窗标题 */ /** 弹窗标题 */
title?: string; 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 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 { useSimpleLocale } from '@vben-core/composables';
import { import {
@@ -30,6 +30,7 @@ import { cn } from '@vben-core/shared/utils';
const props = withDefaults(defineProps<AlertProps>(), { const props = withDefaults(defineProps<AlertProps>(), {
bordered: true, bordered: true,
buttonAlign: 'end',
centered: true, centered: true,
containerClass: 'w-[520px]', containerClass: 'w-[520px]',
}); });
@@ -38,14 +39,12 @@ const open = defineModel<boolean>('open', { default: false });
const { $t } = useSimpleLocale(); const { $t } = useSimpleLocale();
const components = globalShareState.getComponents(); const components = globalShareState.getComponents();
const isConfirm = ref(false); const isConfirm = ref(false);
watch(open, async (val) => {
await nextTick(); function onAlertClosed() {
if (val) { emits('closed', isConfirm.value);
isConfirm.value = false; isConfirm.value = false;
} else { }
emits('closed', isConfirm.value);
}
});
const getIconRender = computed(() => { const getIconRender = computed(() => {
let iconRender: Component | null = null; let iconRender: Component | null = null;
if (props.icon) { if (props.icon) {
@@ -99,6 +98,7 @@ function handleCancel() {
const loading = ref(false); const loading = ref(false);
async function handleOpenChange(val: boolean) { async function handleOpenChange(val: boolean) {
await nextTick();
if (!val && props.beforeClose) { if (!val && props.beforeClose) {
loading.value = true; loading.value = true;
try { try {
@@ -119,15 +119,16 @@ async function handleOpenChange(val: boolean) {
<AlertDialogContent <AlertDialogContent
:open="open" :open="open"
:centered="centered" :centered="centered"
:overlay-blur="overlayBlur"
@opened="emits('opened')" @opened="emits('opened')"
@closed="onAlertClosed"
:class=" :class="
cn( cn(
containerClass, 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, 'border-border border': bordered,
'shadow-3xl': !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"> <div class="flex items-center">
<component :is="getIconRender" class="mr-2" /> <component :is="getIconRender" class="mr-2" />
<span class="flex-auto">{{ $t(title) }}</span> <span class="flex-auto">{{ $t(title) }}</span>
<AlertDialogCancel v-if="showCancel"> <AlertDialogCancel v-if="showCancel" as-child>
<VbenButton <VbenButton
variant="ghost" variant="ghost"
size="icon" size="icon"
@@ -154,19 +155,24 @@ async function handleOpenChange(val: boolean) {
<div class="m-4 mb-6 min-h-[30px]"> <div class="m-4 mb-6 min-h-[30px]">
<VbenRenderContent :content="content" render-br /> <VbenRenderContent :content="content" render-br />
</div> </div>
<VbenLoading v-if="loading" :spinning="loading" /> <VbenLoading v-if="loading && contentMasking" :spinning="loading" />
</AlertDialogDescription> </AlertDialogDescription>
<div class="flex justify-end gap-x-2"> <div
<AlertDialogCancel v-if="showCancel" :disabled="loading"> class="flex items-center justify-end gap-x-2"
:class="`justify-${buttonAlign}`"
>
<VbenRenderContent :content="footer" />
<AlertDialogCancel v-if="showCancel" as-child>
<component <component
:is="components.DefaultButton || VbenButton" :is="components.DefaultButton || VbenButton"
:disabled="loading"
variant="ghost" variant="ghost"
@click="handleCancel" @click="handleCancel"
> >
{{ cancelText || $t('cancel') }} {{ cancelText || $t('cancel') }}
</component> </component>
</AlertDialogCancel> </AlertDialogCancel>
<AlertDialogAction> <AlertDialogAction as-child>
<component <component
:is="components.PrimaryButton || VbenButton" :is="components.PrimaryButton || VbenButton"
:loading="loading" :loading="loading"

View File

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

View File

@@ -60,6 +60,10 @@ export interface ModalProps {
* 弹窗描述 * 弹窗描述
*/ */
description?: string; description?: string;
/**
* 在关闭时销毁弹窗
*/
destroyOnClose?: boolean;
/** /**
* 是否可拖拽 * 是否可拖拽
* @default false * @default false
@@ -153,10 +157,6 @@ export interface ModalApiOptions extends ModalState {
* 独立的弹窗组件 * 独立的弹窗组件
*/ */
connectedComponent?: Component; connectedComponent?: Component;
/**
* 在关闭时销毁弹窗。仅在使用 connectedComponent 时有效
*/
destroyOnClose?: boolean;
/** /**
* 关闭前的回调,返回 false 可以阻止关闭 * 关闭前的回调,返回 false 可以阻止关闭
* @returns * @returns

View File

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

View File

@@ -1,14 +1,6 @@
import type { ExtendedModalApi, ModalApiOptions, ModalProps } from './modal'; import type { ExtendedModalApi, ModalApiOptions, ModalProps } from './modal';
import { import { defineComponent, h, inject, nextTick, provide, reactive } from 'vue';
defineComponent,
h,
inject,
nextTick,
provide,
reactive,
ref,
} from 'vue';
import { useStore } from '@vben-core/shared/store'; import { useStore } from '@vben-core/shared/store';
@@ -32,7 +24,6 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
const { connectedComponent } = options; const { connectedComponent } = options;
if (connectedComponent) { if (connectedComponent) {
const extendedApi = reactive({}); const extendedApi = reactive({});
const isModalReady = ref(true);
const Modal = defineComponent( const Modal = defineComponent(
(props: TParentModalProps, { attrs, slots }) => { (props: TParentModalProps, { attrs, slots }) => {
provide(USER_MODAL_INJECT_KEY, { provide(USER_MODAL_INJECT_KEY, {
@@ -42,11 +33,6 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
Object.setPrototypeOf(extendedApi, api); Object.setPrototypeOf(extendedApi, api);
}, },
options, options,
async reCreateModal() {
isModalReady.value = false;
await nextTick();
isModalReady.value = true;
},
}); });
checkProps(extendedApi as ExtendedModalApi, { checkProps(extendedApi as ExtendedModalApi, {
...props, ...props,
@@ -55,7 +41,7 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
}); });
return () => return () =>
h( h(
isModalReady.value ? connectedComponent : 'div', connectedComponent,
{ {
...props, ...props,
...attrs, ...attrs,
@@ -84,14 +70,6 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
injectData.options?.onOpenChange?.(isOpen); injectData.options?.onOpenChange?.(isOpen);
}; };
const onClosed = mergedOptions.onClosed;
mergedOptions.onClosed = () => {
onClosed?.();
if (mergedOptions.destroyOnClose) {
injectData.reCreateModal?.();
}
};
const api = new ModalApi(mergedOptions); const api = new ModalApi(mergedOptions);
const extendedApi: ExtendedModalApi = api as never; 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 { computed, ref, watch } from 'vue';
import { Circle, CircleCheckBig, LoaderCircle } from '@vben-core/icons'; import { Circle, CircleCheckBig, LoaderCircle } from '@vben-core/icons';
import { VbenRenderContent } from '@vben-core/shadcn-ui';
import { cn, isFunction } from '@vben-core/shared/utils'; import { cn, isFunction } from '@vben-core/shared/utils';
import { objectOmit } from '@vueuse/core'; import { objectOmit } from '@vueuse/core';
import { VbenRenderContent } from '../render-content';
import VbenButtonGroup from './button-group.vue'; import VbenButtonGroup from './button-group.vue';
import Button from './button.vue'; import Button from './button.vue';

View File

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

View File

@@ -61,7 +61,7 @@ defineExpose({
<template> <template>
<AlertDialogPortal> <AlertDialogPortal>
<Transition name="fade"> <Transition name="fade" appear>
<AlertDialogOverlay <AlertDialogOverlay
v-if="open && modal" v-if="open && modal"
:style="{ :style="{
@@ -80,7 +80,17 @@ defineExpose({
v-bind="forwarded" v-bind="forwarded"
:class=" :class="
cn( 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, props.class,
) )
" "

View File

@@ -17,6 +17,14 @@
".": { ".": {
"types": "./src/index.ts", "types": "./src/index.ts",
"default": "./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": { "dependencies": {

View File

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

View File

@@ -1,9 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ComponentInternalInstance } from 'vue';
import type { VerificationProps } from '../types'; import type { VerificationProps } from '../types';
import { import {
type ComponentInternalInstance,
getCurrentInstance, getCurrentInstance,
nextTick, nextTick,
onMounted, onMounted,
@@ -21,6 +20,44 @@ import { resetSize } from '../utils/util';
* VerifyPoints * VerifyPoints
* @description 点选 * @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,
// },
// // 弹出式pop固定fixed
// mode: {
// default: 'fixed',
// type: String,
// },
// // 间隔
// vSpace: {
// default: 5,
// type: Number,
// },
// });
defineOptions({ defineOptions({
name: 'VerifyPoints', name: 'VerifyPoints',
}); });
@@ -90,7 +127,7 @@ onMounted(() => {
const canvas = ref(null); const canvas = ref(null);
// 获取坐标 // 获取坐标
const getMousePos = function (_obj: any, e: any) { const getMousePos = function (obj: any, e: any) {
const x = e.offsetX; const x = e.offsetX;
const y = e.offsetY; const y = e.offsetY;
return { x, y }; return { x, y };
@@ -153,7 +190,7 @@ function canvasClick(e: any) {
if (res.repCode === '0000') { if (res.repCode === '0000') {
barAreaColor.value = '#4cae4c'; barAreaColor.value = '#4cae4c';
barAreaBorderColor.value = '#5cb85c'; barAreaBorderColor.value = '#5cb85c';
text.value = $t('ui.captcha.sliderSuccessText'); text.value = $t('ui.captcha.success');
bindingClick.value = false; bindingClick.value = false;
if (mode.value === 'pop') { if (mode.value === 'pop') {
setTimeout(() => { setTimeout(() => {
@@ -190,7 +227,7 @@ async function getPictrue() {
backToken.value = res.data.repData.token; backToken.value = res.data.repData.token;
secretKey.value = res.data.repData.secretKey; secretKey.value = res.data.repData.secretKey;
poinTextList.value = res.data.repData.wordList; 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 { } else {
text.value = res?.data?.repMsg; text.value = res?.data?.repMsg;
} }

View File

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

View File

@@ -5,6 +5,7 @@ import type {
RouteLocationNormalizedLoadedGeneric, RouteLocationNormalizedLoadedGeneric,
} from 'vue-router'; } from 'vue-router';
import { computed } from 'vue';
import { RouterView } from 'vue-router'; import { RouterView } from 'vue-router';
import { preferences, usePreferences } from '@vben/preferences'; import { preferences, usePreferences } from '@vben/preferences';
@@ -20,6 +21,15 @@ const { keepAlive } = usePreferences();
const { getCachedTabs, getExcludeCachedTabs, renderRouteView } = const { getCachedTabs, getExcludeCachedTabs, renderRouteView } =
storeToRefs(tabbarStore); storeToRefs(tabbarStore);
/**
* 是否使用动画
*/
const getEnabledTransition = computed(() => {
const { transition } = preferences;
const transitionName = transition.name;
return transitionName && transition.enable;
});
// 页面切换动画 // 页面切换动画
function getTransitionName(_route: RouteLocationNormalizedLoaded) { function getTransitionName(_route: RouteLocationNormalizedLoaded) {
// 如果偏好设置未设置,则不使用动画 // 如果偏好设置未设置,则不使用动画
@@ -90,7 +100,12 @@ function transformComponent(
<div class="relative h-full"> <div class="relative h-full">
<IFrameRouterView /> <IFrameRouterView />
<RouterView v-slot="{ Component, route }"> <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 <KeepAlive
v-if="keepAlive" v-if="keepAlive"
:exclude="getExcludeCachedTabs" :exclude="getExcludeCachedTabs"
@@ -109,6 +124,25 @@ function transformComponent(
:key="route.fullPath" :key="route.fullPath"
/> />
</Transition> </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> </RouterView>
</div> </div>
</template> </template>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -31,6 +31,10 @@ export interface VxeTableGridOptions<T = any> extends VxeTableGridProps<T> {
toolbarConfig?: ToolbarConfigOptions; toolbarConfig?: ToolbarConfigOptions;
} }
export interface SeparatorOptions {
show?: boolean;
backgroundColor?: string;
}
export interface VxeGridProps { export interface VxeGridProps {
/** /**
* 标题 * 标题
@@ -64,6 +68,10 @@ export interface VxeGridProps {
* 显示搜索表单 * 显示搜索表单
*/ */
showSearchForm?: boolean; showSearchForm?: boolean;
/**
* 搜索表单与表格主体之间的分隔条
*/
separator?: boolean | SeparatorOptions;
} }
export type ExtendedVxeGridApi = VxeGridApi & { export type ExtendedVxeGridApi = VxeGridApi & {

View File

@@ -29,7 +29,13 @@ import { usePriorityValues } from '@vben/hooks';
import { EmptyIcon } from '@vben/icons'; import { EmptyIcon } from '@vben/icons';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { usePreferences } from '@vben/preferences'; 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'; import { VbenHelpTooltip, VbenLoading } from '@vben-core/shadcn-ui';
@@ -67,10 +73,30 @@ const {
tableTitle, tableTitle,
tableTitleHelp, tableTitleHelp,
showSearchForm, showSearchForm,
separator,
} = usePriorityValues(props, state); } = usePriorityValues(props, state);
const { isMobile } = usePreferences(); 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 slots: SetupContext['slots'] = useSlots();
const [Form, formApi] = useTableForm({ const [Form, formApi] = useTableForm({
@@ -375,7 +401,18 @@ onUnmounted(() => {
<div <div
v-if="formOptions" v-if="formOptions"
v-show="showSearchForm !== false" 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"> <slot name="form">
<Form> <Form>
@@ -404,6 +441,10 @@ onUnmounted(() => {
</Form> </Form>
</slot> </slot>
<div <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" 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>
</div> </div>

View File

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

View File

@@ -1,14 +1,12 @@
import { createApp, watchEffect } from 'vue'; import { createApp, watchEffect } from 'vue';
import { registerAccessDirective } from '@vben/access'; import { registerAccessDirective } from '@vben/access';
import { initTippy, registerLoadingDirective } from '@vben/common-ui'; import { registerLoadingDirective } from '@vben/common-ui';
import { MotionPlugin } from '@vben/plugins/motion';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores'; import { initStores } from '@vben/stores';
import '@vben/styles'; import '@vben/styles';
import '@vben/styles/antd'; import '@vben/styles/antd';
import { VueQueryPlugin } from '@tanstack/vue-query';
import { useTitle } from '@vueuse/core'; import { useTitle } from '@vueuse/core';
import { $t, setupI18n } from '#/locales'; import { $t, setupI18n } from '#/locales';
@@ -21,13 +19,13 @@ async function bootstrap(namespace: string) {
// 初始化组件适配器 // 初始化组件适配器
await initComponentAdapter(); await initComponentAdapter();
// // 设置弹窗的默认配置 // 设置弹窗的默认配置
// setDefaultModalProps({ // setDefaultModalProps({
// fullscreenButton: false, // fullscreenButton: false,
// }); // });
// // 设置抽屉的默认配置 // 设置抽屉的默认配置
// setDefaultDrawerProps({ // setDefaultDrawerProps({
// // zIndex: 1020, // zIndex: 1020,
// }); // });
const app = createApp(App); const app = createApp(App);
@@ -48,15 +46,18 @@ async function bootstrap(namespace: string) {
registerAccessDirective(app); registerAccessDirective(app);
// 初始化 tippy // 初始化 tippy
const { initTippy } = await import('@vben/common-ui/es/tippy');
initTippy(app); initTippy(app);
// 配置路由及路由守卫 // 配置路由及路由守卫
app.use(router); app.use(router);
// 配置@tanstack/vue-query // 配置@tanstack/vue-query
const { VueQueryPlugin } = await import('@tanstack/vue-query');
app.use(VueQueryPlugin); app.use(VueQueryPlugin);
// 配置Motion插件 // 配置Motion插件
const { MotionPlugin } = await import('@vben/plugins/motion');
app.use(MotionPlugin); 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 { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
import { AuthPageLayout, BasicLayout } from '#/layouts';
import { $t } from '#/locales'; 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页面 */ /** 全局404页面 */
const fallbackNotFoundRoute: RouteRecordRaw = { const fallbackNotFoundRoute: RouteRecordRaw = {
component: () => import('#/views/_core/fallback/not-found.vue'), component: () => import('#/views/_core/fallback/not-found.vue'),
@@ -50,7 +50,7 @@ const coreRoutes: RouteRecordRaw[] = [
{ {
name: 'Login', name: 'Login',
path: 'login', path: 'login',
component: Login, component: () => import('#/views/_core/authentication/login.vue'),
meta: { meta: {
title: $t('page.auth.login'), title: $t('page.auth.login'),
}, },

View File

@@ -74,6 +74,7 @@ const [BaseForm, baseFormApi] = useVbenForm({
}, },
// 菜单接口 // 菜单接口
api: getAllMenusApi, api: getAllMenusApi,
autoSelect: 'first',
}, },
// 字段名 // 字段名
fieldName: 'api', fieldName: 'api',

View File

@@ -16,15 +16,18 @@ const [Modal, modalApi] = useVbenModal({
}, },
onOpenChange(isOpen) { onOpenChange(isOpen) {
if (isOpen) { if (isOpen) {
handleUpdate(10); handleUpdate();
} }
}, },
}); });
function handleUpdate(len: number) { function handleUpdate(len?: number) {
modalApi.setState({ confirmDisabled: true, loading: true }); modalApi.setState({ confirmDisabled: true, loading: true });
setTimeout(() => { 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 }); modalApi.setState({ confirmDisabled: false, loading: false });
}, 2000); }, 2000);
} }
@@ -40,7 +43,7 @@ function handleUpdate(len: number) {
{{ item }} {{ item }}
</div> </div>
<template #prepend-footer> <template #prepend-footer>
<Button type="link" @click="handleUpdate(6)">点击更新数据</Button> <Button type="link" @click="handleUpdate()">点击更新数据</Button>
</template> </template>
</Modal> </Modal>
</template> </template>

View File

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

View File

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

2437
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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