47 Commits

Author SHA1 Message Date
dap
4dc7543bb6 docs: changelog 2025-04-11 13:29:27 +08:00
dap
d8e7945f9f docs: changelog 2025-04-11 13:24:34 +08:00
dap
2fd1fdcb32 refactor: 更改header参数ClientID命名 2025-04-11 11:33:23 +08:00
dap
44ba945a12 fix: 无法点击遮罩关闭 2025-04-09 15:07:14 +08:00
dap
2680101872 fix: 无法点击遮罩关闭 2025-04-09 15:06:00 +08:00
dap
1c2e27613c refactor: 富文本/上传同步改为异步组件导入 2025-04-09 10:03:47 +08:00
dap
3e7a2336b0 Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev 2025-04-09 09:34:17 +08:00
dap
022d5182d7 fix: onCancel -> onClosed 2025-04-09 09:22:13 +08:00
Netfan
329a176a5c perf: optimize bootstrap modules to speed up first-screen loading (#5899)
优化首屏加载速度
2025-04-09 01:05:20 +08:00
dap
41962ef380 docs: changelog 2025-04-08 21:07:09 +08:00
dap
9003df713c fix: vxe新版需要单独设置headerCellConfig 2025-04-08 21:04:01 +08:00
dap
ebb4738be7 refactor: 流程定义 useBeforeCloseDiff 2025-04-08 20:58:09 +08:00
dap
ad7c33a7d6 refactor: 流程分类 useBeforeCloseDiff 2025-04-08 20:55:34 +08:00
dap
a114335a56 refactor: oss配置 useBeforeCloseDiff 2025-04-08 20:53:44 +08:00
Netfan
9379093a4f feat: customizable table separator (#5898)
* 表格的分隔条支持定制背景色或完全移除
2025-04-08 20:28:50 +08:00
ming4762
c9014d0338 perf: 优化关闭页面切换动画的tab切换性能 (#5883) 2025-04-08 20:27:03 +08:00
dap
b8ec8edb38 update: 字典 colorpicker 2025-04-08 19:33:35 +08:00
Netfan
ed26dca64e chore: update pnpm-lock.yaml 2025-04-08 16:31:41 +08:00
Netfan
08c6496e24 chore: update deps 2025-04-08 14:56:40 +08:00
Netfan
a8c5df38e9 fix: possible circular reference issue during build (#5894)
* 修复构建期间出现的循环引用警告
2025-04-08 14:50:05 +08:00
dap
5b9f647cfd update: [vxe table v4.12.5] 参数 "row-config.height" 已废弃,请使用 "cell-config.height" 2025-04-08 13:29:06 +08:00
dap
ae6bf6ee53 refactor: 用户drawer Promise逻辑重构 2025-04-08 12:03:15 +08:00
dap
77894d5df4 update: i18n更新 2025-04-08 11:09:07 +08:00
dap
ba8f36a2c0 update: 移除老版本的不需要组件/代码 2025-04-08 11:04:12 +08:00
dap
133abe9ded refactor: 角色权限 useBeforeCloseDiff 2025-04-08 11:02:36 +08:00
dap
ef390ae636 refactor: 租户套餐useBeforeCloseDiff 2025-04-08 10:57:08 +08:00
dap
6d2f4e8486 refactor: 租户管理 useBeforeCloseDiff 2025-04-08 10:54:28 +08:00
dap
c4962aaf85 refactor: 客户端管理useBeforeCloseDiff 2025-04-08 10:51:16 +08:00
dap
f7128b099e refactor: 通知公告 useBeforeCloseDiff 2025-04-08 10:47:18 +08:00
dap
5510b6dea4 refactor: 字典useBeforeCloseDiff 2025-04-08 10:40:32 +08:00
dap
98f658d46f refactor: 部门管理useBeforeCloseDiff 2025-04-08 10:34:26 +08:00
dap
e307db2f3d refactor: useBeforeCloseDiff 2025-04-08 10:30:56 +08:00
dap
e6dab8300d refactor: 角色管理 useBeforeCloseDiff 2025-04-08 10:22:21 +08:00
dap
eb9f278e7f refactor: useBeforeCloseDiff 2025-04-08 10:10:15 +08:00
dap
34e5812de9 update: vxe active color 2025-04-07 19:37:11 +08:00
dap
07587c0faf update: 用户管理 表单更新(非最终方案) 2025-04-07 19:02:28 +08:00
dap
88316d7498 refactor: useBeforeCloseDiff逻辑更新 2025-04-07 18:48:46 +08:00
dap
53e02d46c2 docs: changelog 2025-04-07 18:44:42 +08:00
dap
5e1de6fc79 fix: 表格固定高度 getVxePopupContainer 2025-04-07 18:41:23 +08:00
dap
7463df053a update: 去除字典动画 2025-04-07 18:25:21 +08:00
dap
1286b52135 fix: getVxePopupContainer 2025-04-07 17:21:49 +08:00
dap
92fe406ae9 update: 字典loading 2025-04-07 17:20:41 +08:00
dap
5b72d9b79d refactor: 移除deepWatch参数 2025-04-07 13:05:30 +08:00
dap
b97fe47afd fix: 直接使用.value无法触发useForm的更新(原生是正常的) 需要修改地址 2025-04-07 12:53:20 +08:00
dap
4f2354b53a update: 兼容以前代码 先返回body 这样会造成无法跟随滚动 2025-04-07 11:10:10 +08:00
dap
8f9006c96d fix: vxe 右上角toolbar按钮色/翻页主题色保持一致 2025-04-07 10:58:51 +08:00
Netfan
71e8d12b70 fix: improve prompt component (#5879)
* fix: prompt component render fixed

* fix: alert buttonAlign default value
2025-04-07 01:21:30 +08:00
60 changed files with 1081 additions and 416 deletions

14
.vscode/settings.json vendored
View File

@@ -224,10 +224,20 @@
"commentTranslate.multiLineMerge": true, "commentTranslate.multiLineMerge": true,
"vue.server.hybridMode": true, "vue.server.hybridMode": true,
"vitest.disableWorkspaceWarning": true, "vitest.disableWorkspaceWarning": true,
"cSpell.words": ["tinymce", "vditor"],
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": "node_modules/typescript/lib",
"editor.linkedEditing": true, // 自动同步更改html标签, "editor.linkedEditing": true, // 自动同步更改html标签,
"vscodeCustomCodeColor.highlightValue": "v-access", // v-access显示的颜色 "vscodeCustomCodeColor.highlightValue": "v-access", // v-access显示的颜色
"vscodeCustomCodeColor.highlightValueColor": "#CCFFFF", "vscodeCustomCodeColor.highlightValueColor": "#CCFFFF",
"oxc.enable": false "oxc.enable": false,
"cSpell.words": [
"archiver",
"axios",
"dotenv",
"isequal",
"jspm",
"napi",
"nolebase",
"rollup",
"vitest"
]
} }

View File

@@ -1,3 +1,23 @@
# 1.3.1
**REFACTOR**
- 所有Modal/Drawer表单关闭前会进行表单数据对比来弹出提示框
- 字典项颜色选择从`原生input type=color`改为`vue3-colorpicker`组件
- 全局Header: ClientID 更改大小写 [spring的问题导致](https://gitee.com/dapppp/ruoyi-plus-vben5/issues/IC0BDS)
**BUG FIX**
- getVxePopupContainer逻辑调整 解决表格固定高度展开不全的问题
**FEATURES**
- 字典渲染支持loading(length为0情况)
**OTHERS**
- useForm的组件改为异步导入(官方更新) bootstrap.js体积从2M降到600K 首屏加载速度提升
# 1.3.0 # 1.3.0
注意: 如果你使用老版本的`文件上传`/`图片上传` 可暂时使用 注意: 如果你使用老版本的`文件上传`/`图片上传` 可暂时使用

View File

@@ -54,7 +54,8 @@
"tinymce": "^7.3.0", "tinymce": "^7.3.0",
"unplugin-vue-components": "^0.27.3", "unplugin-vue-components": "^0.27.3",
"vue": "catalog:", "vue": "catalog:",
"vue-router": "catalog:" "vue-router": "catalog:",
"vue3-colorpicker": "^2.3.0"
}, },
"devDependencies": { "devDependencies": {
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",

View File

@@ -8,40 +8,80 @@ 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 { computed, defineComponent, getCurrentInstance, h, ref } from 'vue'; import {
computed,
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,
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 { Tinymce as RichTextarea } from '#/components/tinymce';
import { FileUpload, ImageUpload } from '#/components/upload';
import { FileUploadOld, ImageUploadOld } from '#/components/upload-old'; import { FileUploadOld, ImageUploadOld } from '#/components/upload-old';
const RichTextarea = defineAsyncComponent(() =>
import('#/components/tinymce/index').then((res) => res.Tinymce),
);
const FileUpload = defineAsyncComponent(() =>
import('#/components/upload').then((res) => res.FileUpload),
);
const ImageUpload = defineAsyncComponent(() =>
import('#/components/upload').then((res) => res.ImageUpload),
);
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>( const withDefaultPlaceholder = <T extends Component>(
component: T, component: T,
type: 'input' | 'select', type: 'input' | 'select',

View File

@@ -93,9 +93,12 @@ function createRequestClient(baseURL: string) {
const language = preferences.app.locale.replace('-', '_'); const language = preferences.app.locale.replace('-', '_');
config.headers['Accept-Language'] = language; config.headers['Accept-Language'] = language;
config.headers['Content-Language'] = language; config.headers['Content-Language'] = language;
// 添加全局clientId /**
config.headers.clientId = clientId; * 添加全局clientId
* 关于header的clientId被错误绑定到实体类
* https://gitee.com/dapppp/ruoyi-plus-vben5/issues/IC0BDS
*/
config.headers.ClientID = clientId;
/** /**
* 格式化get/delete参数 * 格式化get/delete参数
* 如果包含自定义的paramsSerializer则不走此逻辑 * 如果包含自定义的paramsSerializer则不走此逻辑

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';
@@ -50,12 +49,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

@@ -4,7 +4,7 @@ import type { DictData } from '#/api/system/dict/dict-data-model';
import { computed } from 'vue'; import { computed } from 'vue';
import { Tag } from 'ant-design-vue'; import { Spin, Tag } from 'ant-design-vue';
import { tagTypes } from './data'; import { tagTypes } from './data';
@@ -41,12 +41,22 @@ const label = computed<number | string>(() => {
}); });
const tagComponent = computed(() => (color.value ? Tag : 'div')); const tagComponent = computed(() => (color.value ? Tag : 'div'));
const loading = computed(() => {
return props.dicts?.length === 0;
});
</script> </script>
<template> <template>
<div> <div>
<component :is="tagComponent" :class="cssClass" :color="color"> <component
v-if="!loading"
:is="tagComponent"
:class="cssClass"
:color="color"
>
{{ label }} {{ label }}
</component> </component>
<Spin v-else :spinning="true" size="small" />
</div> </div>
</template> </template>

View File

@@ -204,7 +204,12 @@ export function useUpload(
if (props.maxCount === 1) { if (props.maxCount === 1) {
bindValue.value = ossId; bindValue.value = ossId;
} else { } else {
(bindValue.value as string[]).push(ossId); // 给默认值
if (!Array.isArray(bindValue.value)) {
bindValue.value = [];
}
// 直接使用.value无法触发useForm的更新(原生是正常的) 需要修改地址
bindValue.value = [...bindValue.value, ossId];
} }
break; break;
} }
@@ -344,12 +349,16 @@ export function useUpload(
!props.keepMissingId && !props.keepMissingId &&
props.maxCount !== 1 props.maxCount !== 1
) { ) {
bindValue.value = (bindValue.value as string[]).filter((ossId) => // 给默认值
if (!Array.isArray(bindValue.value)) {
bindValue.value = [];
}
bindValue.value = bindValue.value.filter((ossId) =>
resp.map((res) => res.ossId).includes(ossId), resp.map((res) => res.ossId).includes(ossId),
); );
} }
}, },
{ immediate: true, deep: props.deepWatch }, { immediate: true },
); );
return { return {

View File

@@ -87,13 +87,6 @@ export interface BaseUploadProps {
* @default false * @default false
*/ */
enableDragUpload?: boolean; enableDragUpload?: boolean;
/**
* 是否开启深度监听
* 默认外部的数组地址重新改变才会触发watch 不会监听内部元素的变化
* 开启后 无论内部还是外部改变都会触发查询信息接口(包括上传后, 删除等操作都会触发)
* @default false
*/
deepWatch?: boolean;
/** /**
* 当ossId查询不到文件信息时 比如被删除了 * 当ossId查询不到文件信息时 比如被删除了
* 是否保留列表对应的ossId 默认不保留 * 是否保留列表对应的ossId 默认不保留

View File

@@ -21,6 +21,7 @@
"preview": "Preview", "preview": "Preview",
"tip": "Tip", "tip": "Tip",
"enable": "On", "enable": "On",
"disable": "Off" "disable": "Off",
"beforeCloseTip": "You have unsaved changes. Are you sure you want to exit?"
} }
} }

View File

@@ -21,6 +21,7 @@
"preview": "预览", "preview": "预览",
"tip": "提示", "tip": "提示",
"enable": "启用", "enable": "启用",
"disable": "禁用" "disable": "禁用",
"beforeCloseTip": "您有未保存的更改,确认要退出吗?"
} }
} }

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'),
@@ -58,7 +58,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

@@ -59,6 +59,7 @@ export const useDictStore = defineStore('app-dict', () => {
} }
function resetCache() { function resetCache() {
dictRequestCache.clear();
dictOptionsMap.clear(); dictOptionsMap.clear();
/** /**
* 不需要清空dictRequestCache 每次请求成功/失败都清空key * 不需要清空dictRequestCache 每次请求成功/失败都清空key

View File

@@ -29,43 +29,52 @@ interface BeforeCloseDiffProps {
} }
/** /**
* @deprecated 注意为实验性功能 可能有api变动/被移除 * 用于Drawer/Modal使用 判断表单是否有变动来决定是否弹窗提示
* @param props props * @param props props
* @returns hook * @returns hook
*
* 待解决问题: 网速慢情况直接关闭 会导致数据不一致问题
* 但是使用api.lock会导致在报错情况无法关闭(因为目前代码没有finally)
*/ */
export function useBeforeCloseDiff(props: BeforeCloseDiffProps) { export function useBeforeCloseDiff(props: BeforeCloseDiffProps) {
const { initializedGetter, currentGetter, compare } = props; const { initializedGetter, currentGetter, compare } = props;
/**
* 记录初始值 json
*/
const initialized = ref<string>(''); const initialized = ref<string>('');
/**
* 是否已经初始化了 通过这个值判断是否需要进行对比 为false直接关闭 不弹窗
*/
const isInitialized = ref(false); const isInitialized = ref(false);
const isSubmitted = ref(false);
async function updateInitialized(data?: string) { /**
* 标记是否已经完成初始化 后续需要进行对比
* @param data 自定义初始化数据 可选
*/
async function markInitialized(data?: string) {
initialized.value = data || (await initializedGetter()); initialized.value = data || (await initializedGetter());
isInitialized.value = true; isInitialized.value = true;
} }
function setSubmitted() { /**
isSubmitted.value = true; * 重置初始化状态 需要在closed前调用 或者打开窗口时
*/
function resetInitialized() {
initialized.value = '';
isInitialized.value = false;
} }
/**
* 提供给useVbenForm/useVbenDrawer使用
* @returns 是否允许关闭
*/
async function onBeforeClose(): Promise<boolean> { async function onBeforeClose(): Promise<boolean> {
// 如果还未初始化,直接允许关闭 // 如果还未初始化,直接允许关闭
if (!isInitialized.value) { if (!isInitialized.value) {
return true; return true;
} }
// 如果已经提交过,直接允许关闭
if (isSubmitted.value) {
// 重置状态
isSubmitted.value = false;
return true;
}
try { try {
// 获取当前表单数据
const current = await currentGetter(); const current = await currentGetter();
// 自定义比较的情况
if (isFunction(compare) && compare(initialized.value, current)) { if (isFunction(compare) && compare(initialized.value, current)) {
return true; return true;
} else { } else {
@@ -79,7 +88,7 @@ export function useBeforeCloseDiff(props: BeforeCloseDiffProps) {
return new Promise<boolean>((resolve) => { return new Promise<boolean>((resolve) => {
Modal.confirm({ Modal.confirm({
title: $t('pages.common.tip'), title: $t('pages.common.tip'),
content: $t('您有未保存的更改,确认要退出吗?'), content: $t('pages.common.beforeCloseTip'),
centered: true, centered: true,
okButtonProps: { danger: true }, okButtonProps: { danger: true },
cancelText: $t('common.cancel'), cancelText: $t('common.cancel'),
@@ -99,8 +108,8 @@ export function useBeforeCloseDiff(props: BeforeCloseDiffProps) {
return { return {
onBeforeClose, onBeforeClose,
updateInitialized, markInitialized,
setSubmitted, resetInitialized,
}; };
} }

View File

@@ -7,6 +7,7 @@ import { cloneDeep } from '@vben/utils';
import { useVbenForm } from '#/adapter/form'; import { useVbenForm } from '#/adapter/form';
import { clientAdd, clientInfo, clientUpdate } from '#/api/system/client'; import { clientAdd, clientInfo, clientUpdate } from '#/api/system/client';
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
import { drawerSchema } from './data'; import { drawerSchema } from './data';
import SecretInput from './secret-input.vue'; import SecretInput from './secret-input.vue';
@@ -55,6 +56,13 @@ function setupForm(update: boolean) {
]); ]);
} }
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
{
initializedGetter: defaultFormValueGetter(formApi),
currentGetter: defaultFormValueGetter(formApi),
},
);
// 提取生成状态字段Schema的函数 // 提取生成状态字段Schema的函数
const getStatusSchema = (disabled: boolean) => [ const getStatusSchema = (disabled: boolean) => [
{ {
@@ -64,13 +72,15 @@ const getStatusSchema = (disabled: boolean) => [
]; ];
const [BasicDrawer, drawerApi] = useVbenDrawer({ const [BasicDrawer, drawerApi] = useVbenDrawer({
onCancel: handleCancel, onBeforeClose,
onClosed: handleClosed,
onConfirm: handleConfirm, onConfirm: handleConfirm,
async onOpenChange(isOpen) { async onOpenChange(isOpen) {
if (!isOpen) { if (!isOpen) {
return null; return null;
} }
drawerApi.drawerLoading(true); drawerApi.drawerLoading(true);
const { id } = drawerApi.getData() as { id?: number | string }; const { id } = drawerApi.getData() as { id?: number | string };
isUpdate.value = !!id; isUpdate.value = !!id;
// 初始化 // 初始化
@@ -84,36 +94,39 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
// 新增模式: 确保状态字段可用 // 新增模式: 确保状态字段可用
formApi.updateSchema(getStatusSchema(false)); formApi.updateSchema(getStatusSchema(false));
} }
await markInitialized();
drawerApi.drawerLoading(false); drawerApi.drawerLoading(false);
}, },
}); });
async function handleConfirm() { async function handleConfirm() {
try { try {
drawerApi.drawerLoading(true); drawerApi.lock(true);
const { valid } = await formApi.validate(); const { valid } = await formApi.validate();
if (!valid) { if (!valid) {
return; return;
} }
const data = cloneDeep(await formApi.getValues()); const data = cloneDeep(await formApi.getValues());
await (isUpdate.value ? clientUpdate(data) : clientAdd(data)); await (isUpdate.value ? clientUpdate(data) : clientAdd(data));
resetInitialized();
emit('reload'); emit('reload');
await handleCancel(); drawerApi.close();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally { } finally {
drawerApi.drawerLoading(false); drawerApi.lock(false);
} }
} }
async function handleCancel() { async function handleClosed() {
drawerApi.close();
await formApi.resetForm(); await formApi.resetForm();
resetInitialized();
} }
</script> </script>
<template> <template>
<BasicDrawer :close-on-click-modal="false" :title="title" class="w-[600px]"> <BasicDrawer :title="title" class="w-[600px]">
<BasicForm> <BasicForm>
<template #clientSecret="slotProps"> <template #clientSecret="slotProps">
<SecretInput v-bind="slotProps" :disabled="isUpdate" /> <SecretInput v-bind="slotProps" :disabled="isUpdate" />

View File

@@ -26,10 +26,12 @@ const [BasicForm, formApi] = useVbenForm({
showDefaultActions: false, showDefaultActions: false,
}); });
const { onBeforeClose, updateInitialized, setSubmitted } = useBeforeCloseDiff({ const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
initializedGetter: defaultFormValueGetter(formApi), {
currentGetter: defaultFormValueGetter(formApi), initializedGetter: defaultFormValueGetter(formApi),
}); currentGetter: defaultFormValueGetter(formApi),
},
);
const [BasicModal, modalApi] = useVbenModal({ const [BasicModal, modalApi] = useVbenModal({
fullscreenButton: false, fullscreenButton: false,
@@ -40,22 +42,18 @@ const [BasicModal, modalApi] = useVbenModal({
if (!isOpen) { if (!isOpen) {
return null; return null;
} }
try { modalApi.modalLoading(true);
modalApi.lock(true);
const { id } = modalApi.getData() as { id?: number | string }; const { id } = modalApi.getData() as { id?: number | string };
isUpdate.value = !!id; isUpdate.value = !!id;
if (isUpdate.value && id) { if (isUpdate.value && id) {
const record = await configInfo(id); const record = await configInfo(id);
await formApi.setValues(record); await formApi.setValues(record);
}
await updateInitialized();
} catch (error) {
console.error(error);
} finally {
modalApi.lock(false);
} }
await markInitialized();
modalApi.modalLoading(false);
}, },
}); });
@@ -68,7 +66,7 @@ async function handleConfirm() {
} }
const data = cloneDeep(await formApi.getValues()); const data = cloneDeep(await formApi.getValues());
await (isUpdate.value ? configUpdate(data) : configAdd(data)); await (isUpdate.value ? configUpdate(data) : configAdd(data));
setSubmitted(); resetInitialized();
emit('reload'); emit('reload');
modalApi.close(); modalApi.close();
} catch (error) { } catch (error) {
@@ -80,6 +78,7 @@ async function handleConfirm() {
async function handleClosed() { async function handleClosed() {
await formApi.resetForm(); await formApi.resetForm();
resetInitialized();
} }
</script> </script>

View File

@@ -16,6 +16,7 @@ import {
deptUpdate, deptUpdate,
} from '#/api/system/dept'; } from '#/api/system/dept';
import { listUserByDeptId } from '#/api/system/user'; import { listUserByDeptId } from '#/api/system/user';
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
import { drawerSchema } from './data'; import { drawerSchema } from './data';
@@ -107,8 +108,16 @@ async function setLeaderOptions() {
]); ]);
} }
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
{
initializedGetter: defaultFormValueGetter(formApi),
currentGetter: defaultFormValueGetter(formApi),
},
);
const [BasicDrawer, drawerApi] = useVbenDrawer({ const [BasicDrawer, drawerApi] = useVbenDrawer({
onCancel: handleCancel, onBeforeClose,
onClosed: handleClosed,
onConfirm: handleConfirm, onConfirm: handleConfirm,
async onOpenChange(isOpen) { async onOpenChange(isOpen) {
if (!isOpen) { if (!isOpen) {
@@ -130,6 +139,7 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
await (update && id ? initDeptUsers(id) : setLeaderOptions()); await (update && id ? initDeptUsers(id) : setLeaderOptions());
/** 部门选择 下拉框 */ /** 部门选择 下拉框 */
await initDeptSelect(id); await initDeptSelect(id);
await markInitialized();
drawerApi.drawerLoading(false); drawerApi.drawerLoading(false);
}, },
@@ -137,30 +147,31 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
async function handleConfirm() { async function handleConfirm() {
try { try {
drawerApi.drawerLoading(true); drawerApi.lock(true);
const { valid } = await formApi.validate(); const { valid } = await formApi.validate();
if (!valid) { if (!valid) {
return; return;
} }
const data = cloneDeep(await formApi.getValues()); const data = cloneDeep(await formApi.getValues());
await (isUpdate.value ? deptUpdate(data) : deptAdd(data)); await (isUpdate.value ? deptUpdate(data) : deptAdd(data));
resetInitialized();
emit('reload'); emit('reload');
await handleCancel(); drawerApi.close();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally { } finally {
drawerApi.drawerLoading(false); drawerApi.lock(false);
} }
} }
async function handleCancel() { async function handleClosed() {
drawerApi.close();
await formApi.resetForm(); await formApi.resetForm();
resetInitialized();
} }
</script> </script>
<template> <template>
<BasicDrawer :close-on-click-modal="false" :title="title" class="w-[600px]"> <BasicDrawer :title="title" class="w-[600px]">
<BasicForm /> <BasicForm />
</BasicDrawer> </BasicDrawer>
</template> </template>

View File

@@ -12,6 +12,7 @@ import {
dictDetailInfo, dictDetailInfo,
} from '#/api/system/dict/dict-data'; } from '#/api/system/dict/dict-data';
import { tagTypes } from '#/components/dict'; import { tagTypes } from '#/components/dict';
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
import { drawerSchema } from './data'; import { drawerSchema } from './data';
import TagStylePicker from './tag-style-picker.vue'; import TagStylePicker from './tag-style-picker.vue';
@@ -57,8 +58,16 @@ function setupSelectType(listClass: string) {
selectType.value = isDefault ? 'default' : 'custom'; selectType.value = isDefault ? 'default' : 'custom';
} }
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
{
initializedGetter: defaultFormValueGetter(formApi),
currentGetter: defaultFormValueGetter(formApi),
},
);
const [BasicDrawer, drawerApi] = useVbenDrawer({ const [BasicDrawer, drawerApi] = useVbenDrawer({
onCancel: handleCancel, onBeforeClose,
onClosed: handleClosed,
onConfirm: handleConfirm, onConfirm: handleConfirm,
async onOpenChange(isOpen) { async onOpenChange(isOpen) {
if (!isOpen) { if (!isOpen) {
@@ -75,6 +84,7 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
setupSelectType(record.listClass); setupSelectType(record.listClass);
await formApi.setValues(record); await formApi.setValues(record);
} }
await markInitialized();
drawerApi.drawerLoading(false); drawerApi.drawerLoading(false);
}, },
@@ -82,7 +92,7 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
async function handleConfirm() { async function handleConfirm() {
try { try {
drawerApi.drawerLoading(true); drawerApi.lock(true);
const { valid } = await formApi.validate(); const { valid } = await formApi.validate();
if (!valid) { if (!valid) {
return; return;
@@ -93,19 +103,20 @@ async function handleConfirm() {
data.listClass = ''; data.listClass = '';
} }
await (isUpdate.value ? dictDataUpdate(data) : dictDataAdd(data)); await (isUpdate.value ? dictDataUpdate(data) : dictDataAdd(data));
resetInitialized();
emit('reload'); emit('reload');
await handleCancel(); drawerApi.close();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally { } finally {
drawerApi.drawerLoading(false); drawerApi.lock(false);
} }
} }
async function handleCancel() { async function handleClosed() {
drawerApi.close();
await formApi.resetForm(); await formApi.resetForm();
selectType.value = 'default'; selectType.value = 'default';
resetInitialized();
} }
/** /**
@@ -117,7 +128,7 @@ async function handleDeSelect() {
</script> </script>
<template> <template>
<BasicDrawer :close-on-click-modal="false" :title="title" class="w-[600px]"> <BasicDrawer :title="title" class="w-[600px]">
<BasicForm> <BasicForm>
<template #listClass="slotProps"> <template #listClass="slotProps">
<TagStylePicker <TagStylePicker

View File

@@ -1,14 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import type { RadioChangeEvent } from 'ant-design-vue'; import type { RadioChangeEvent } from 'ant-design-vue';
import type { PropType } from 'vue';
import { computed } from 'vue'; import { computed } from 'vue';
import { Input, RadioGroup, Select } from 'ant-design-vue'; import { usePreferences } from '@vben/preferences';
import { RadioGroup, Select } from 'ant-design-vue';
import { ColorPicker } from 'vue3-colorpicker';
import { tagSelectOptions } from '#/components/dict'; import { tagSelectOptions } from '#/components/dict';
import 'vue3-colorpicker/style.css';
/** /**
* 需要禁止透传 * 需要禁止透传
* 不禁止会有奇怪的bug 会绑定到selectType上 * 不禁止会有奇怪的bug 会绑定到selectType上
@@ -32,23 +35,26 @@ const computedOptions = computed(
type SelectType = (typeof options)[number]['value']; type SelectType = (typeof options)[number]['value'];
const selectType = defineModel('selectType', { const selectType = defineModel<SelectType>('selectType', {
default: 'default', default: 'default',
type: String as PropType<SelectType>,
}); });
/** /**
* color必须为hex颜色或者undefined * color必须为hex颜色或者undefined
*/ */
const color = defineModel('value', { const color = defineModel<string | undefined>('value', {
default: undefined, default: undefined,
type: String as PropType<string | undefined>,
}); });
function handleSelectTypeChange(e: RadioChangeEvent) { function handleSelectTypeChange(e: RadioChangeEvent) {
// 必须给默认hex颜色 不能为空字符串 // 必须给默认hex颜色 不能为空字符串
color.value = e.target.value === 'custom' ? '#000000' : undefined; color.value = e.target.value === 'custom' ? '#1677ff' : undefined;
} }
const { isDark } = usePreferences();
const theme = computed(() => {
return isDark.value ? 'black' : 'white';
});
</script> </script>
<template> <template>
@@ -69,15 +75,12 @@ function handleSelectTypeChange(e: RadioChangeEvent) {
placeholder="请选择标签样式" placeholder="请选择标签样式"
@deselect="$emit('deselect')" @deselect="$emit('deselect')"
/> />
<Input <ColorPicker
v-if="selectType === 'custom'" v-if="selectType === 'custom'"
v-model:value="color" disable-alpha
class="flex-1" format="hex"
disabled v-model:pure-color="color"
> :theme="theme"
<template #addonAfter> />
<input v-model="color" class="rounded-lg" type="color" />
</template>
</Input>
</div> </div>
</template> </template>

View File

@@ -71,7 +71,6 @@ export const modalSchema: FormSchemaGetter = () => [
{ {
component: 'Textarea', component: 'Textarea',
fieldName: 'remark', fieldName: 'remark',
formItemClass: 'items-start',
label: '备注', label: '备注',
}, },
]; ];

View File

@@ -11,6 +11,7 @@ import {
dictTypeInfo, dictTypeInfo,
dictTypeUpdate, dictTypeUpdate,
} from '#/api/system/dict/dict-type'; } from '#/api/system/dict/dict-type';
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
import { modalSchema } from './data'; import { modalSchema } from './data';
@@ -22,6 +23,7 @@ const title = computed(() => {
}); });
const [BasicForm, formApi] = useVbenForm({ const [BasicForm, formApi] = useVbenForm({
layout: 'vertical',
commonConfig: { commonConfig: {
labelWidth: 100, labelWidth: 100,
}, },
@@ -29,51 +31,63 @@ const [BasicForm, formApi] = useVbenForm({
showDefaultActions: false, showDefaultActions: false,
}); });
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
{
initializedGetter: defaultFormValueGetter(formApi),
currentGetter: defaultFormValueGetter(formApi),
},
);
const [BasicModal, modalApi] = useVbenModal({ const [BasicModal, modalApi] = useVbenModal({
fullscreenButton: false, fullscreenButton: false,
onCancel: handleCancel, onBeforeClose,
onClosed: handleClosed,
onConfirm: handleConfirm, onConfirm: handleConfirm,
onOpenChange: async (isOpen) => { onOpenChange: async (isOpen) => {
if (!isOpen) { if (!isOpen) {
return null; return null;
} }
modalApi.modalLoading(true); modalApi.modalLoading(true);
const { id } = modalApi.getData() as { id?: number | string }; const { id } = modalApi.getData() as { id?: number | string };
isUpdate.value = !!id; isUpdate.value = !!id;
if (isUpdate.value && id) { if (isUpdate.value && id) {
const record = await dictTypeInfo(id); const record = await dictTypeInfo(id);
await formApi.setValues(record); await formApi.setValues(record);
} }
await markInitialized();
modalApi.modalLoading(false); modalApi.modalLoading(false);
}, },
}); });
async function handleConfirm() { async function handleConfirm() {
try { try {
modalApi.modalLoading(true); modalApi.lock(true);
const { valid } = await formApi.validate(); const { valid } = await formApi.validate();
if (!valid) { if (!valid) {
return; return;
} }
const data = cloneDeep(await formApi.getValues()); const data = cloneDeep(await formApi.getValues());
await (isUpdate.value ? dictTypeUpdate(data) : dictTypeAdd(data)); await (isUpdate.value ? dictTypeUpdate(data) : dictTypeAdd(data));
resetInitialized();
emit('reload'); emit('reload');
await handleCancel(); modalApi.close();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally { } finally {
modalApi.modalLoading(false); modalApi.lock(false);
} }
} }
async function handleCancel() { async function handleClosed() {
modalApi.close();
await formApi.resetForm(); await formApi.resetForm();
resetInitialized();
} }
</script> </script>
<template> <template>
<BasicModal :close-on-click-modal="false" :title="title"> <BasicModal :title="title">
<BasicForm /> <BasicForm />
</BasicModal> </BasicModal>
</template> </template>

View File

@@ -12,6 +12,7 @@ import {
import { useVbenForm } from '#/adapter/form'; import { useVbenForm } from '#/adapter/form';
import { menuAdd, menuInfo, menuList, menuUpdate } from '#/api/system/menu'; import { menuAdd, menuInfo, menuList, menuUpdate } from '#/api/system/menu';
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
import { drawerSchema } from './data'; import { drawerSchema } from './data';
@@ -88,14 +89,23 @@ async function setupMenuSelect() {
]); ]);
} }
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
{
initializedGetter: defaultFormValueGetter(formApi),
currentGetter: defaultFormValueGetter(formApi),
},
);
const [BasicDrawer, drawerApi] = useVbenDrawer({ const [BasicDrawer, drawerApi] = useVbenDrawer({
onCancel: handleCancel, onBeforeClose,
onClosed: handleClosed,
onConfirm: handleConfirm, onConfirm: handleConfirm,
async onOpenChange(isOpen) { async onOpenChange(isOpen) {
if (!isOpen) { if (!isOpen) {
return null; return null;
} }
drawerApi.drawerLoading(true); drawerApi.drawerLoading(true);
const { id, update } = drawerApi.getData() as ModalProps; const { id, update } = drawerApi.getData() as ModalProps;
isUpdate.value = update; isUpdate.value = update;
@@ -108,36 +118,39 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
await formApi.setValues(record); await formApi.setValues(record);
} }
} }
await markInitialized();
drawerApi.drawerLoading(false); drawerApi.drawerLoading(false);
}, },
}); });
async function handleConfirm() { async function handleConfirm() {
try { try {
drawerApi.drawerLoading(true); drawerApi.lock(true);
const { valid } = await formApi.validate(); const { valid } = await formApi.validate();
if (!valid) { if (!valid) {
return; return;
} }
const data = cloneDeep(await formApi.getValues()); const data = cloneDeep(await formApi.getValues());
await (isUpdate.value ? menuUpdate(data) : menuAdd(data)); await (isUpdate.value ? menuUpdate(data) : menuAdd(data));
resetInitialized();
emit('reload'); emit('reload');
await handleCancel(); drawerApi.close();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally { } finally {
drawerApi.drawerLoading(false); drawerApi.lock(false);
} }
} }
async function handleCancel() { async function handleClosed() {
drawerApi.close();
await formApi.resetForm(); await formApi.resetForm();
resetInitialized();
} }
</script> </script>
<template> <template>
<BasicDrawer :close-on-click-modal="false" :title="title" class="w-[600px]"> <BasicDrawer :title="title" class="w-[600px]">
<BasicForm /> <BasicForm />
</BasicDrawer> </BasicDrawer>
</template> </template>

View File

@@ -18,6 +18,7 @@ import { pick } from 'lodash-es';
import { noticeAdd, noticeInfo, noticeUpdate } from '#/api/system/notice'; import { noticeAdd, noticeInfo, noticeUpdate } from '#/api/system/notice';
import { Tinymce } from '#/components/tinymce'; import { Tinymce } from '#/components/tinymce';
import { getDictOptions } from '#/utils/dict'; import { getDictOptions } from '#/utils/dict';
import { useBeforeCloseDiff } from '#/utils/popup';
const emit = defineEmits<{ reload: [] }>(); const emit = defineEmits<{ reload: [] }>();
@@ -74,17 +75,29 @@ const { validate, validateInfos, resetFields } = Form.useForm(
formRules, formRules,
); );
function customFormValueGetter() {
return JSON.stringify(formData.value);
}
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
{
initializedGetter: customFormValueGetter,
currentGetter: customFormValueGetter,
},
);
const [BasicModal, modalApi] = useVbenModal({ const [BasicModal, modalApi] = useVbenModal({
class: 'w-[800px]', class: 'w-[800px]',
fullscreenButton: true, fullscreenButton: true,
closeOnClickModal: false, onBeforeClose,
onClosed: handleCancel, onClosed: handleClosed,
onConfirm: handleConfirm, onConfirm: handleConfirm,
onOpenChange: async (isOpen) => { onOpenChange: async (isOpen) => {
if (!isOpen) { if (!isOpen) {
return null; return null;
} }
modalApi.modalLoading(true); modalApi.modalLoading(true);
const { id } = modalApi.getData() as { id?: number | string }; const { id } = modalApi.getData() as { id?: number | string };
isUpdate.value = !!id; isUpdate.value = !!id;
if (isUpdate.value && id) { if (isUpdate.value && id) {
@@ -93,30 +106,33 @@ const [BasicModal, modalApi] = useVbenModal({
const filterRecord = pick(record, Object.keys(defaultValues)); const filterRecord = pick(record, Object.keys(defaultValues));
formData.value = filterRecord; formData.value = filterRecord;
} }
await markInitialized();
modalApi.modalLoading(false); modalApi.modalLoading(false);
}, },
}); });
async function handleConfirm() { async function handleConfirm() {
try { try {
modalApi.modalLoading(true); modalApi.lock(true);
await validate(); await validate();
// 可能会做数据处理 使用cloneDeep深拷贝 // 可能会做数据处理 使用cloneDeep深拷贝
const data = cloneDeep(formData.value); const data = cloneDeep(formData.value);
await (isUpdate.value ? noticeUpdate(data) : noticeAdd(data)); await (isUpdate.value ? noticeUpdate(data) : noticeAdd(data));
resetInitialized();
emit('reload'); emit('reload');
await handleCancel(); modalApi.close();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally { } finally {
modalApi.modalLoading(false); modalApi.lock(false);
} }
} }
async function handleCancel() { async function handleClosed() {
modalApi.close();
formData.value = defaultValues; formData.value = defaultValues;
resetFields(); resetFields();
resetInitialized();
} }
</script> </script>

View File

@@ -13,6 +13,7 @@ import {
ossConfigInfo, ossConfigInfo,
ossConfigUpdate, ossConfigUpdate,
} from '#/api/system/oss-config'; } from '#/api/system/oss-config';
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
import { drawerSchema } from './data'; import { drawerSchema } from './data';
@@ -33,27 +34,38 @@ const [BasicForm, formApi] = useVbenForm({
wrapperClass: 'grid-cols-3', wrapperClass: 'grid-cols-3',
}); });
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
{
initializedGetter: defaultFormValueGetter(formApi),
currentGetter: defaultFormValueGetter(formApi),
},
);
const [BasicDrawer, drawerApi] = useVbenDrawer({ const [BasicDrawer, drawerApi] = useVbenDrawer({
onCancel: handleCancel, onBeforeClose,
onClosed: handleClosed,
onConfirm: handleConfirm, onConfirm: handleConfirm,
async onOpenChange(isOpen) { async onOpenChange(isOpen) {
if (!isOpen) { if (!isOpen) {
return null; return null;
} }
drawerApi.drawerLoading(true); drawerApi.drawerLoading(true);
const { id } = drawerApi.getData() as { id?: number | string }; const { id } = drawerApi.getData() as { id?: number | string };
isUpdate.value = !!id; isUpdate.value = !!id;
if (isUpdate.value && id) { if (isUpdate.value && id) {
const record = await ossConfigInfo(id); const record = await ossConfigInfo(id);
await formApi.setValues(record); await formApi.setValues(record);
} }
await markInitialized();
drawerApi.drawerLoading(false); drawerApi.drawerLoading(false);
}, },
}); });
async function handleConfirm() { async function handleConfirm() {
try { try {
drawerApi.drawerLoading(true); drawerApi.lock(true);
/** /**
* 这里解构出来的values只能获取到自定义校验参数的值 * 这里解构出来的values只能获取到自定义校验参数的值
* 需要自行调用formApi.getValues()获取表单值 * 需要自行调用formApi.getValues()获取表单值
@@ -64,23 +76,24 @@ async function handleConfirm() {
} }
const data = cloneDeep(await formApi.getValues()); const data = cloneDeep(await formApi.getValues());
await (isUpdate.value ? ossConfigUpdate(data) : ossConfigAdd(data)); await (isUpdate.value ? ossConfigUpdate(data) : ossConfigAdd(data));
resetInitialized();
emit('reload'); emit('reload');
await handleCancel(); drawerApi.close();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally { } finally {
drawerApi.drawerLoading(false); drawerApi.lock(false);
} }
} }
async function handleCancel() { async function handleClosed() {
drawerApi.close();
await formApi.resetForm(); await formApi.resetForm();
resetInitialized();
} }
</script> </script>
<template> <template>
<BasicDrawer :close-on-click-modal="false" :title="title" class="w-[650px]"> <BasicDrawer :title="title" class="w-[650px]">
<BasicForm> <BasicForm>
<template #tip> <template #tip>
<div class="ml-7 w-full"> <div class="ml-7 w-full">

View File

@@ -83,6 +83,9 @@ const gridOptions: VxeGridProps = {
}, },
}, },
}, },
headerCellConfig: {
height: 44,
},
cellConfig: { cellConfig: {
height: 65, height: 65,
}, },

View File

@@ -96,7 +96,7 @@ async function handleCancel() {
</script> </script>
<template> <template>
<BasicDrawer :close-on-click-modal="false" :title="title" class="w-[600px]"> <BasicDrawer :title="title" class="w-[600px]">
<BasicForm /> <BasicForm />
</BasicDrawer> </BasicDrawer>
</template> </template>

View File

@@ -9,6 +9,7 @@ import { cloneDeep } from '@vben/utils';
import { useVbenForm } from '#/adapter/form'; import { useVbenForm } from '#/adapter/form';
import { roleDataScope, roleDeptTree, roleInfo } from '#/api/system/role'; import { roleDataScope, roleDeptTree, roleInfo } from '#/api/system/role';
import { TreeSelectPanel } from '#/components/tree'; import { TreeSelectPanel } from '#/components/tree';
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
import { authModalSchemas } from './data'; import { authModalSchemas } from './data';
@@ -33,9 +34,25 @@ async function setupDeptTree(id: number | string) {
deptTree.value = resp.depts; deptTree.value = resp.depts;
} }
async function customFormValueGetter() {
const v = await defaultFormValueGetter(formApi)();
// 获取勾选信息
const menuIds = deptSelectRef.value?.[0]?.getCheckedKeys() ?? [];
const mixStr = v + menuIds.join(',');
return mixStr;
}
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
{
initializedGetter: customFormValueGetter,
currentGetter: customFormValueGetter,
},
);
const [BasicModal, modalApi] = useVbenModal({ const [BasicModal, modalApi] = useVbenModal({
fullscreenButton: false, fullscreenButton: false,
onCancel: handleCancel, onBeforeClose,
onCancel: handleClosed,
onConfirm: handleConfirm, onConfirm: handleConfirm,
onOpenChange: async (isOpen) => { onOpenChange: async (isOpen) => {
if (!isOpen) { if (!isOpen) {
@@ -48,6 +65,7 @@ const [BasicModal, modalApi] = useVbenModal({
setupDeptTree(id); setupDeptTree(id);
const record = await roleInfo(id); const record = await roleInfo(id);
await formApi.setValues(record); await formApi.setValues(record);
markInitialized();
modalApi.modalLoading(false); modalApi.modalLoading(false);
}, },
@@ -60,7 +78,7 @@ const deptSelectRef = ref();
async function handleConfirm() { async function handleConfirm() {
try { try {
modalApi.modalLoading(true); modalApi.lock(true);
const { valid } = await formApi.validate(); const { valid } = await formApi.validate();
if (!valid) { if (!valid) {
return; return;
@@ -75,18 +93,19 @@ async function handleConfirm() {
data.deptIds = []; data.deptIds = [];
} }
await roleDataScope(data); await roleDataScope(data);
resetInitialized();
emit('reload'); emit('reload');
await handleCancel(); modalApi.close();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally { } finally {
modalApi.modalLoading(false); modalApi.lock(false);
} }
} }
async function handleCancel() { async function handleClosed() {
modalApi.close();
await formApi.resetForm(); await formApi.resetForm();
resetInitialized();
} }
/** /**
@@ -99,11 +118,7 @@ function handleCheckStrictlyChange(value: boolean) {
</script> </script>
<template> <template>
<BasicModal <BasicModal class="min-h-[600px] w-[550px]" title="分配权限">
:close-on-click-modal="false"
class="min-h-[600px] w-[550px]"
title="分配权限"
>
<BasicForm> <BasicForm>
<template #deptIds="slotProps"> <template #deptIds="slotProps">
<TreeSelectPanel <TreeSelectPanel

View File

@@ -1,3 +1,6 @@
<!--
TODO: 这个页面要优化逻辑
-->
<script setup lang="ts"> <script setup lang="ts">
import type { MenuOption } from '#/api/system/menu/model'; import type { MenuOption } from '#/api/system/menu/model';
@@ -11,6 +14,7 @@ import { useVbenForm } from '#/adapter/form';
import { menuTreeSelect, roleMenuTreeSelect } from '#/api/system/menu'; import { menuTreeSelect, roleMenuTreeSelect } from '#/api/system/menu';
import { roleAdd, roleInfo, roleUpdate } from '#/api/system/role'; import { roleAdd, roleInfo, roleUpdate } from '#/api/system/role';
import { MenuSelectTable } from '#/components/tree'; import { MenuSelectTable } from '#/components/tree';
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
import { drawerSchema } from './data'; import { drawerSchema } from './data';
@@ -62,14 +66,31 @@ async function setupMenuTree(id?: number | string) {
} }
} }
async function customFormValueGetter() {
const v = await defaultFormValueGetter(formApi)();
// 获取勾选信息
const menuIds = menuSelectRef.value?.getCheckedKeys?.() ?? [];
const mixStr = v + menuIds.join(',');
return mixStr;
}
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
{
initializedGetter: customFormValueGetter,
currentGetter: customFormValueGetter,
},
);
const [BasicDrawer, drawerApi] = useVbenDrawer({ const [BasicDrawer, drawerApi] = useVbenDrawer({
onCancel: handleCancel, onBeforeClose,
onClosed: handleClosed,
onConfirm: handleConfirm, onConfirm: handleConfirm,
async onOpenChange(isOpen) { async onOpenChange(isOpen) {
if (!isOpen) { if (!isOpen) {
return null; return null;
} }
drawerApi.drawerLoading(true); drawerApi.drawerLoading(true);
const { id } = drawerApi.getData() as { id?: number | string }; const { id } = drawerApi.getData() as { id?: number | string };
isUpdate.value = !!id; isUpdate.value = !!id;
@@ -79,6 +100,7 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
} }
// init菜单 注意顺序要放在赋值record之后 内部watch会依赖record // init菜单 注意顺序要放在赋值record之后 内部watch会依赖record
await setupMenuTree(id); await setupMenuTree(id);
await markInitialized();
drawerApi.drawerLoading(false); drawerApi.drawerLoading(false);
}, },
@@ -87,7 +109,8 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
const menuSelectRef = ref<InstanceType<typeof MenuSelectTable>>(); const menuSelectRef = ref<InstanceType<typeof MenuSelectTable>>();
async function handleConfirm() { async function handleConfirm() {
try { try {
drawerApi.drawerLoading(true); drawerApi.lock(true);
const { valid } = await formApi.validate(); const { valid } = await formApi.validate();
if (!valid) { if (!valid) {
return; return;
@@ -99,17 +122,18 @@ async function handleConfirm() {
data.menuIds = menuIds; data.menuIds = menuIds;
await (isUpdate.value ? roleUpdate(data) : roleAdd(data)); await (isUpdate.value ? roleUpdate(data) : roleAdd(data));
emit('reload'); emit('reload');
await handleCancel(); resetInitialized();
drawerApi.close();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally { } finally {
drawerApi.drawerLoading(false); drawerApi.lock(false);
} }
} }
async function handleCancel() { async function handleClosed() {
drawerApi.close();
await formApi.resetForm(); await formApi.resetForm();
resetInitialized();
} }
/** /**
@@ -122,7 +146,7 @@ function handleMenuCheckStrictlyChange(value: boolean) {
</script> </script>
<template> <template>
<BasicDrawer :close-on-click-modal="false" :title="title" class="w-[800px]"> <BasicDrawer :title="title" class="w-[800px]">
<BasicForm> <BasicForm>
<template #menuIds="slotProps"> <template #menuIds="slotProps">
<div class="h-[600px] w-full"> <div class="h-[600px] w-full">

View File

@@ -9,6 +9,7 @@ import { useVbenForm } from '#/adapter/form';
import { tenantAdd, tenantInfo, tenantUpdate } from '#/api/system/tenant'; import { tenantAdd, tenantInfo, tenantUpdate } from '#/api/system/tenant';
import { packageSelectList } from '#/api/system/tenant-package'; import { packageSelectList } from '#/api/system/tenant-package';
import { useTenantStore } from '#/store/tenant'; import { useTenantStore } from '#/store/tenant';
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
import { drawerSchema } from './data'; import { drawerSchema } from './data';
@@ -51,22 +52,33 @@ async function setupPackageSelect() {
]); ]);
} }
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
{
initializedGetter: defaultFormValueGetter(formApi),
currentGetter: defaultFormValueGetter(formApi),
},
);
const [BasicDrawer, drawerApi] = useVbenDrawer({ const [BasicDrawer, drawerApi] = useVbenDrawer({
onCancel: handleCancel, onBeforeClose,
onClosed: handleClosed,
onConfirm: handleConfirm, onConfirm: handleConfirm,
async onOpenChange(isOpen) { async onOpenChange(isOpen) {
if (!isOpen) { if (!isOpen) {
return null; return null;
} }
drawerApi.drawerLoading(true); drawerApi.drawerLoading(true);
const { id } = drawerApi.getData() as { id?: number | string }; const { id } = drawerApi.getData() as { id?: number | string };
isUpdate.value = !!id; isUpdate.value = !!id;
// 初始化 // 初始化
await setupPackageSelect(); await setupPackageSelect();
if (isUpdate.value && id) { if (isUpdate.value && id) {
const record = await tenantInfo(id); const record = await tenantInfo(id);
await formApi.setValues(record); await formApi.setValues(record);
} }
formApi.updateSchema([ formApi.updateSchema([
{ {
fieldName: 'packageId', fieldName: 'packageId',
@@ -75,6 +87,8 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
}, },
}, },
]); ]);
await markInitialized();
drawerApi.drawerLoading(false); drawerApi.drawerLoading(false);
}, },
}); });
@@ -82,32 +96,33 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
const tenantStore = useTenantStore(); const tenantStore = useTenantStore();
async function handleConfirm() { async function handleConfirm() {
try { try {
drawerApi.drawerLoading(true); drawerApi.lock(true);
const { valid } = await formApi.validate(); const { valid } = await formApi.validate();
if (!valid) { if (!valid) {
return; return;
} }
const data = cloneDeep(await formApi.getValues()); const data = cloneDeep(await formApi.getValues());
await (isUpdate.value ? tenantUpdate(data) : tenantAdd(data)); await (isUpdate.value ? tenantUpdate(data) : tenantAdd(data));
resetInitialized();
emit('reload'); emit('reload');
await handleCancel(); drawerApi.close();
// 重新加载租户信息 // 重新加载租户信息
tenantStore.initTenant(); tenantStore.initTenant();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally { } finally {
drawerApi.drawerLoading(false); drawerApi.lock(false);
} }
} }
async function handleCancel() { async function handleClosed() {
drawerApi.close();
await formApi.resetForm(); await formApi.resetForm();
resetInitialized();
} }
</script> </script>
<template> <template>
<BasicDrawer :close-on-click-modal="false" :title="title" class="w-[600px]"> <BasicDrawer :title="title" class="w-[600px]">
<BasicForm /> <BasicForm />
</BasicDrawer> </BasicDrawer>
</template> </template>

View File

@@ -65,12 +65,6 @@ export const drawerSchema: FormSchemaGetter = () => [
{ {
component: 'Textarea', component: 'Textarea',
fieldName: 'remark', fieldName: 'remark',
formItemClass: 'items-start',
label: '备注', label: '备注',
}, },
]; ];
// 租户管理 不可分配 只有superadmin有权限操作 分配了也没用
export const excludeIds = [
6, 121, 122, 1606, 1607, 1608, 1609, 1610, 1611, 1612, 1613, 1614, 1615,
];

View File

@@ -17,6 +17,7 @@ import {
packageUpdate, packageUpdate,
} from '#/api/system/tenant-package'; } from '#/api/system/tenant-package';
import { MenuSelectTable } from '#/components/tree'; import { MenuSelectTable } from '#/components/tree';
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
import { drawerSchema } from './data'; import { drawerSchema } from './data';
@@ -65,8 +66,24 @@ async function setupMenuTree(id?: number | string) {
} }
} }
async function customFormValueGetter() {
const v = await defaultFormValueGetter(formApi)();
// 获取勾选信息
const menuIds = menuSelectRef.value?.getCheckedKeys?.() ?? [];
const mixStr = v + menuIds.join(',');
return mixStr;
}
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
{
initializedGetter: customFormValueGetter,
currentGetter: customFormValueGetter,
},
);
const [BasicDrawer, drawerApi] = useVbenDrawer({ const [BasicDrawer, drawerApi] = useVbenDrawer({
onCancel: handleCancel, onBeforeClose,
onClosed: handleClosed,
onConfirm: handleConfirm, onConfirm: handleConfirm,
async onOpenChange(isOpen) { async onOpenChange(isOpen) {
if (!isOpen) { if (!isOpen) {
@@ -84,6 +101,7 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
} }
// init菜单 注意顺序要放在赋值record之后 内部watch会依赖record // init菜单 注意顺序要放在赋值record之后 内部watch会依赖record
await setupMenuTree(id); await setupMenuTree(id);
await markInitialized();
drawerApi.drawerLoading(false); drawerApi.drawerLoading(false);
}, },
@@ -103,8 +121,9 @@ async function handleConfirm() {
const data = cloneDeep(await formApi.getValues()); const data = cloneDeep(await formApi.getValues());
data.menuIds = menuIds; data.menuIds = menuIds;
await (isUpdate.value ? packageUpdate(data) : packageAdd(data)); await (isUpdate.value ? packageUpdate(data) : packageAdd(data));
resetInitialized();
emit('reload'); emit('reload');
await handleCancel(); drawerApi.close();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally { } finally {
@@ -112,9 +131,9 @@ async function handleConfirm() {
} }
} }
async function handleCancel() { async function handleClosed() {
drawerApi.close();
await formApi.resetForm(); await formApi.resetForm();
resetInitialized();
} }
/** /**
@@ -127,7 +146,7 @@ function handleMenuCheckStrictlyChange(value: boolean) {
</script> </script>
<template> <template>
<BasicDrawer :close-on-click-modal="false" :title="title" class="w-[800px]"> <BasicDrawer :title="title" class="w-[800px]">
<BasicForm> <BasicForm>
<template #menuIds="slotProps"> <template #menuIds="slotProps">
<div class="h-[600px] w-full"> <div class="h-[600px] w-full">

View File

@@ -1,44 +0,0 @@
import type { PropType } from 'vue';
import type { Menu } from '#/api/system/menu/model';
import { computed, defineComponent } from 'vue';
import { Tag } from 'ant-design-vue';
export default defineComponent({
name: 'TreeItem',
props: {
data: {
required: true,
type: Object as PropType<Menu>,
},
},
setup(props, { expose }) {
expose();
interface TagProp {
color: string;
text: string;
}
const menuTagProp = computed<TagProp>(() => {
// 正则判断是否为链接
if (/^https?:\/\/[^\s/$.?#].\S*$/i.test(props.data.path)) {
return { color: 'pink', text: '外链' };
}
const type = props.data.menuType;
if (type === 'M') return { color: 'green', text: '目录' };
if (type === 'C') return { color: 'blue', text: '菜单' };
if (type === 'F') return { color: '', text: '按钮' };
return { color: 'error', text: '未知' };
});
return () => (
<div class="flex gap-[6px]">
<span>{props.data.menuName}</span>
<Tag color={menuTagProp.value.color}>{menuTagProp.value.text}</Tag>
</div>
);
},
});

View File

@@ -113,6 +113,9 @@ const gridOptions: VxeGridProps = {
}, },
}, },
}, },
headerCellConfig: {
height: 44,
},
cellConfig: { cellConfig: {
height: 48, height: 48,
}, },

View File

@@ -18,6 +18,7 @@ import {
userAdd, userAdd,
userUpdate, userUpdate,
} from '#/api/system/user'; } from '#/api/system/user';
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
import { authScopeOptions } from '#/views/system/role/data'; import { authScopeOptions } from '#/views/system/role/data';
import { drawerSchema } from './data'; import { drawerSchema } from './data';
@@ -134,8 +135,16 @@ async function loadDefaultPassword(update: boolean) {
} }
} }
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
{
initializedGetter: defaultFormValueGetter(formApi),
currentGetter: defaultFormValueGetter(formApi),
},
);
const [BasicDrawer, drawerApi] = useVbenDrawer({ const [BasicDrawer, drawerApi] = useVbenDrawer({
onCancel: handleCancel, onBeforeClose,
onClosed: handleClosed,
onConfirm: handleConfirm, onConfirm: handleConfirm,
async onOpenChange(isOpen) { async onOpenChange(isOpen) {
if (!isOpen) { if (!isOpen) {
@@ -149,6 +158,7 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
return null; return null;
} }
drawerApi.drawerLoading(true); drawerApi.drawerLoading(true);
const { id } = drawerApi.getData() as { id?: number | string }; const { id } = drawerApi.getData() as { id?: number | string };
isUpdate.value = !!id; isUpdate.value = !!id;
/** update时 禁用用户名修改 不显示密码框 */ /** update时 禁用用户名修改 不显示密码框 */
@@ -186,10 +196,11 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
fieldName: 'postIds', fieldName: 'postIds',
}, },
]); ]);
// 部门选择 && 初始密码
await Promise.all([setupDeptSelect(), loadDefaultPassword(isUpdate.value)]); // 部门选择、初始密码及用户相关操作并行处理
const promises = [setupDeptSelect(), loadDefaultPassword(isUpdate.value)];
if (user) { if (user) {
await Promise.all([ promises.push(
// 添加基础信息 // 添加基础信息
formApi.setValues(user), formApi.setValues(user),
// 添加角色和岗位 // 添加角色和岗位
@@ -197,38 +208,43 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
formApi.setFieldValue('roleIds', roleIds), formApi.setFieldValue('roleIds', roleIds),
// 更新时不会触发onSelect 需要手动调用 // 更新时不会触发onSelect 需要手动调用
setupPostOptions(user.deptId), setupPostOptions(user.deptId),
]); );
} }
// 并行处理 重构后会带来10-50ms的优化
await Promise.all(promises);
await markInitialized();
drawerApi.drawerLoading(false); drawerApi.drawerLoading(false);
}, },
}); });
async function handleConfirm() { async function handleConfirm() {
try { try {
drawerApi.drawerLoading(true); drawerApi.lock(true);
const { valid } = await formApi.validate(); const { valid } = await formApi.validate();
if (!valid) { if (!valid) {
return; return;
} }
const data = cloneDeep(await formApi.getValues()); const data = cloneDeep(await formApi.getValues());
await (isUpdate.value ? userUpdate(data) : userAdd(data)); await (isUpdate.value ? userUpdate(data) : userAdd(data));
resetInitialized();
emit('reload'); emit('reload');
await handleCancel(); drawerApi.close();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally { } finally {
drawerApi.drawerLoading(false); drawerApi.lock(false);
} }
} }
async function handleCancel() { async function handleClosed() {
drawerApi.close(); formApi.resetForm();
await formApi.resetForm(); resetInitialized();
} }
</script> </script>
<template> <template>
<BasicDrawer :close-on-click-modal="false" :title="title" class="w-[600px]"> <BasicDrawer :title="title" class="w-[600px]">
<BasicForm /> <BasicForm />
</BasicDrawer> </BasicDrawer>
</template> </template>

View File

@@ -17,6 +17,7 @@ import {
categoryList, categoryList,
categoryUpdate, categoryUpdate,
} from '#/api/workflow/category'; } from '#/api/workflow/category';
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
import { modalSchema } from './data'; import { modalSchema } from './data';
@@ -65,9 +66,17 @@ async function setupCategorySelect() {
]); ]);
} }
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
{
initializedGetter: defaultFormValueGetter(formApi),
currentGetter: defaultFormValueGetter(formApi),
},
);
const [BasicModal, modalApi] = useVbenModal({ const [BasicModal, modalApi] = useVbenModal({
fullscreenButton: false, fullscreenButton: false,
onCancel: handleCancel, onBeforeClose,
onClosed: handleClosed,
onConfirm: handleConfirm, onConfirm: handleConfirm,
onOpenChange: async (isOpen) => { onOpenChange: async (isOpen) => {
if (!isOpen) { if (!isOpen) {
@@ -89,6 +98,7 @@ const [BasicModal, modalApi] = useVbenModal({
await formApi.setValues({ parentId }); await formApi.setValues({ parentId });
} }
await setupCategorySelect(); await setupCategorySelect();
await markInitialized();
modalApi.modalLoading(false); modalApi.modalLoading(false);
}, },
@@ -96,7 +106,7 @@ const [BasicModal, modalApi] = useVbenModal({
async function handleConfirm() { async function handleConfirm() {
try { try {
modalApi.modalLoading(true); modalApi.lock(true);
const { valid } = await formApi.validate(); const { valid } = await formApi.validate();
if (!valid) { if (!valid) {
return; return;
@@ -104,27 +114,24 @@ async function handleConfirm() {
// getValues获取为一个readonly的对象 需要修改必须先深拷贝一次 // getValues获取为一个readonly的对象 需要修改必须先深拷贝一次
const data = cloneDeep(await formApi.getValues()); const data = cloneDeep(await formApi.getValues());
await (isUpdate.value ? categoryUpdate(data) : categoryAdd(data)); await (isUpdate.value ? categoryUpdate(data) : categoryAdd(data));
resetInitialized();
emit('reload'); emit('reload');
await handleCancel(); modalApi.close();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally { } finally {
modalApi.modalLoading(false); modalApi.lock(false);
} }
} }
async function handleCancel() { async function handleClosed() {
modalApi.close();
await formApi.resetForm(); await formApi.resetForm();
resetInitialized();
} }
</script> </script>
<template> <template>
<BasicModal <BasicModal :title="title" class="min-h-[500px]">
:close-on-click-modal="false"
:title="title"
class="min-h-[500px]"
>
<BasicForm /> <BasicForm />
</BasicModal> </BasicModal>
</template> </template>

View File

@@ -93,9 +93,14 @@ const gridOptions: VxeGridProps = {
}, },
}, },
}, },
headerCellConfig: {
height: 44,
},
cellConfig: {
height: 100,
},
rowConfig: { rowConfig: {
keyField: 'id', keyField: 'id',
height: 100,
}, },
id: 'workflow-definition-index', id: 'workflow-definition-index',
}; };

View File

@@ -12,6 +12,7 @@ import {
workflowDefinitionInfo, workflowDefinitionInfo,
workflowDefinitionUpdate, workflowDefinitionUpdate,
} from '#/api/workflow/definition'; } from '#/api/workflow/definition';
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
import { modalSchema } from './data'; import { modalSchema } from './data';
@@ -65,8 +66,16 @@ async function setupCategorySelect() {
]); ]);
} }
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
{
initializedGetter: defaultFormValueGetter(formApi),
currentGetter: defaultFormValueGetter(formApi),
},
);
const [BasicDrawer, modalApi] = useVbenModal({ const [BasicDrawer, modalApi] = useVbenModal({
onCancel: handleCancel, onBeforeClose,
onCancel: handleClosed,
onConfirm: handleConfirm, onConfirm: handleConfirm,
async onOpenChange(isOpen) { async onOpenChange(isOpen) {
if (!isOpen) { if (!isOpen) {
@@ -83,6 +92,7 @@ const [BasicDrawer, modalApi] = useVbenModal({
const record = await workflowDefinitionInfo(id); const record = await workflowDefinitionInfo(id);
await formApi.setValues(record); await formApi.setValues(record);
} }
await markInitialized();
modalApi.modalLoading(false); modalApi.modalLoading(false);
}, },
@@ -90,7 +100,7 @@ const [BasicDrawer, modalApi] = useVbenModal({
async function handleConfirm() { async function handleConfirm() {
try { try {
modalApi.modalLoading(true); modalApi.lock(true);
const { valid } = await formApi.validate(); const { valid } = await formApi.validate();
if (!valid) { if (!valid) {
return; return;
@@ -103,27 +113,23 @@ async function handleConfirm() {
await workflowDefinitionAdd(data); await workflowDefinitionAdd(data);
emit('reload', 'add'); emit('reload', 'add');
} }
await handleCancel(); resetInitialized();
modalApi.close();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally { } finally {
modalApi.modalLoading(false); modalApi.lock(false);
} }
} }
async function handleCancel() { async function handleClosed() {
modalApi.close();
await formApi.resetForm(); await formApi.resetForm();
resetInitialized();
} }
</script> </script>
<template> <template>
<BasicDrawer <BasicDrawer :fullscreen-button="false" :title="title" class="w-[550px]">
:close-on-click-modal="false"
:fullscreen-button="false"
:title="title"
class="w-[550px]"
>
<div class="min-h-[400px]"> <div class="min-h-[400px]">
<BasicForm /> <BasicForm />
</div> </div>

View File

@@ -41,7 +41,7 @@ const formOptions: VbenFormProps = {
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4', wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
handleReset: async () => { handleReset: async () => {
selectedCode.value = []; selectedCode.value = [];
// eslint-disable-next-line no-use-before-define
const { formApi, reload } = tableApi; const { formApi, reload } = tableApi;
await formApi.resetForm(); await formApi.resetForm();
const formValues = formApi.form.values; const formValues = formApi.form.values;
@@ -68,7 +68,7 @@ async function handleTypeChange(e: RadioChangeEvent) {
break; break;
} }
} }
// eslint-disable-next-line no-use-before-define
await tableApi.reload(); await tableApi.reload();
} }
@@ -103,9 +103,14 @@ const gridOptions: VxeGridProps = {
}, },
}, },
}, },
headerCellConfig: {
height: 44,
},
cellConfig: {
height: 66,
},
rowConfig: { rowConfig: {
keyField: 'id', keyField: 'id',
height: 66,
}, },
id: 'workflow-definition-index', id: 'workflow-definition-index',
}; };

View File

@@ -5,7 +5,7 @@ import type { CustomGetter } from '#/components/upload/src/props';
import { h, ref } from 'vue'; import { h, ref } from 'vue';
import { CodeMirror, Page } from '@vben/common-ui'; import { CodeMirror, Page, useVbenModal } from '@vben/common-ui';
import { useClipboard } from '@vueuse/core'; import { useClipboard } from '@vueuse/core';
import { Alert, Card, Modal, RadioGroup, Switch } from 'ant-design-vue'; import { Alert, Card, Modal, RadioGroup, Switch } from 'ant-design-vue';
@@ -14,6 +14,7 @@ import { FileUpload, ImageUpload } from '#/components/upload';
import { useFileType, useImageType } from './hook'; import { useFileType, useImageType } from './hook';
import sql from './insert.sql?raw'; import sql from './insert.sql?raw';
import uploadModal from './upload-modal.vue';
const singleImageId = ref('1905537674682916865'); const singleImageId = ref('1905537674682916865');
const singleFileId = ref('1905191167882518529'); const singleFileId = ref('1905191167882518529');
@@ -53,6 +54,10 @@ const customThumbnailUrl: CustomGetter<undefined> = () => {
const { copy } = useClipboard({ legacy: true }); const { copy } = useClipboard({ legacy: true });
const animationEnable = ref(false); const animationEnable = ref(false);
const [UploadModal, uploadModalApi] = useVbenModal({
connectedComponent: uploadModal,
});
</script> </script>
<template> <template>
@@ -63,6 +68,10 @@ const animationEnable = ref(false);
<CodeMirror class="mt-2" v-model="sql" language="sql" readonly /> <CodeMirror class="mt-2" v-model="sql" language="sql" readonly />
</Card> </Card>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<Card title="表单上传">
<a-button @click="uploadModalApi.open()">打开</a-button>
<UploadModal />
</Card>
<Card title="单上传, 会绑定为string" size="small"> <Card title="单上传, 会绑定为string" size="small">
<ImageUpload v-model:value="singleImageId" /> <ImageUpload v-model:value="singleImageId" />
当前绑定值: {{ singleImageId }} 当前绑定值: {{ singleImageId }}

View File

@@ -0,0 +1,70 @@
<script setup lang="ts">
import { h } from 'vue';
import { JsonPreview, useVbenModal } from '@vben/common-ui';
import { Modal, Space } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
const [BasicForm, formApi] = useVbenForm({
layout: 'vertical',
schema: [
{
label: '图片上传多图',
component: 'ImageUpload',
fieldName: 'ossIds',
componentProps: {
maxCount: 3,
},
},
{
label: '图片上传单图',
component: 'ImageUpload',
fieldName: 'ossId',
componentProps: {
maxCount: 1,
},
},
],
showDefaultActions: false,
});
async function getValues() {
try {
const v = await formApi.getValues();
console.log(v);
Modal.info({
content: () => h(JsonPreview, { data: v }),
});
} catch (error) {
console.error(error);
}
}
async function handleAssign() {
const ids = ['1908761290673315841', '1907738568539332610'];
await formApi.setValues({
ossIds: ids,
ossId: ids[0],
});
}
const [BasicModal] = useVbenModal({
title: '上传',
footer: false,
});
</script>
<template>
<BasicModal>
<div class="flex flex-col">
<Space>
<a-button @click="handleAssign">赋值</a-button>
<a-button @click="getValues">获取值</a-button>
</Space>
<BasicForm />
</div>
</BasicModal>
</template>

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,6 +67,8 @@ export type AlertProps = {
content: Component | string; content: Component | string;
/** 弹窗内容的额外样式 */ /** 弹窗内容的额外样式 */
contentClass?: string; contentClass?: string;
/** 执行beforeClose回调期间在内容区域显示一个loading遮罩*/
contentMasking?: boolean;
/** 弹窗的图标(在标题的前面) */ /** 弹窗的图标(在标题的前面) */
icon?: Component | IconType; icon?: Component | IconType;
/** 是否显示取消按钮 */ /** 是否显示取消按钮 */
@@ -70,6 +77,25 @@ export type AlertProps = {
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

@@ -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,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 } from 'ant-design-vue';
import { BadgeJapaneseYen } from 'lucide-vue-next';
function showPrompt() { function showPrompt() {
prompt({ prompt({
@@ -17,25 +20,62 @@ function showPrompt() {
function showSelectPrompt() { function showSelectPrompt() {
prompt({ prompt({
component: VbenSelect, component: Input,
componentProps: { componentProps: {
placeholder: '请输入',
prefix: '充值金额',
type: 'number',
},
componentSlots: {
addonAfter: () => h(BadgeJapaneseYen),
},
content: '此弹窗演示了如何使用componentSlots传递自定义插槽',
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) {
console.log(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="showSelectPrompt">Prompt With Select</VbenButton>
<VbenButton @click="showAsyncPrompt">Prompt With Async</VbenButton>
</div> </div>
</template> </template>

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,6 +32,8 @@ export type AlertProps = {
content: Component | string; content: Component | string;
/** 弹窗内容的额外样式 */ /** 弹窗内容的额外样式 */
contentClass?: string; contentClass?: string;
/** 执行beforeClose回调期间在内容区域显示一个loading遮罩*/
contentMasking?: boolean;
/** 弹窗的图标(在标题的前面) */ /** 弹窗的图标(在标题的前面) */
icon?: Component | IconType; icon?: Component | IconType;
/** 是否显示取消按钮 */ /** 是否显示取消按钮 */
@@ -32,3 +41,26 @@ export type AlertProps = {
/** 弹窗标题 */ /** 弹窗标题 */
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

@@ -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]',
}); });
@@ -154,9 +155,9 @@ 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 class="flex justify-end gap-x-2" :class="`justify-${buttonAlign}`">
<AlertDialogCancel v-if="showCancel" :disabled="loading"> <AlertDialogCancel v-if="showCancel" :disabled="loading">
<component <component
:is="components.DefaultButton || VbenButton" :is="components.DefaultButton || VbenButton"

View File

@@ -88,10 +88,10 @@ export class DrawerApi {
/** /**
* 关闭弹窗 * 关闭弹窗
*/ */
close() { async close() {
// 通过 onBeforeClose 钩子函数来判断是否允许关闭弹窗 // 通过 onBeforeClose 钩子函数来判断是否允许关闭弹窗
// 如果 onBeforeClose 返回 false则不关闭弹窗 // 如果 onBeforeClose 返回 false则不关闭弹窗
const allowClose = this.api.onBeforeClose?.() ?? true; const allowClose = (await this.api.onBeforeClose?.()) ?? true;
if (allowClose) { if (allowClose) {
this.store.setState((prev) => ({ this.store.setState((prev) => ({
...prev, ...prev,

View File

@@ -1,6 +1,6 @@
import type { Component, Ref } from 'vue'; import type { Component, Ref } from 'vue';
import type { ClassType } from '@vben-core/typings'; import type { ClassType, MaybePromise } from '@vben-core/typings';
import type { DrawerApi } from './drawer-api'; import type { DrawerApi } from './drawer-api';
@@ -151,7 +151,7 @@ export interface DrawerApiOptions extends DrawerState {
* 关闭前的回调,返回 false 可以阻止关闭 * 关闭前的回调,返回 false 可以阻止关闭
* @returns * @returns
*/ */
onBeforeClose?: () => void; onBeforeClose?: () => MaybePromise<boolean | undefined>;
/** /**
* 点击取消按钮的回调 * 点击取消按钮的回调
*/ */

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

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

@@ -5,9 +5,11 @@ import type {
RouteLocationNormalizedLoadedGeneric, RouteLocationNormalizedLoadedGeneric,
} from 'vue-router'; } from 'vue-router';
import { computed } from 'vue';
import { RouterView } from 'vue-router';
import { preferences, usePreferences } from '@vben/preferences'; import { preferences, usePreferences } from '@vben/preferences';
import { storeToRefs, useTabbarStore } from '@vben/stores'; import { storeToRefs, useTabbarStore } from '@vben/stores';
import { RouterView } from 'vue-router';
import { IFrameRouterView } from '../../iframe'; import { IFrameRouterView } from '../../iframe';
@@ -19,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) {
// 如果偏好设置未设置,则不使用动画 // 如果偏好设置未设置,则不使用动画
@@ -89,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"
@@ -108,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

@@ -46,9 +46,13 @@
--vxe-ui-table-row-current-background-color: hsl(var(--accent)); --vxe-ui-table-row-current-background-color: hsl(var(--accent));
--vxe-ui-table-row-hover-current-background-color: hsl(var(--accent-hover)); --vxe-ui-table-row-hover-current-background-color: hsl(var(--accent-hover));
height: auto !important;
/* --vxe-ui-table-fixed-scrolling-box-shadow-color: rgb(0 0 0 / 80%); */ /* --vxe-ui-table-fixed-scrolling-box-shadow-color: rgb(0 0 0 / 80%); */
/** 右上角toolbar按钮色/翻页主题色保持一致 */
--vxe-ui-font-primary-lighten-color: hsl(var(--primary-500));
--vxe-ui-font-primary-darken-color: hsl(var(--primary-600));
height: auto !important;
} }
html[data-vxe-ui-theme='dark'] .vxe-grid { html[data-vxe-ui-theme='dark'] .vxe-grid {

View File

@@ -1,6 +1,3 @@
import type { ClassType, DeepPartial } from '@vben/types';
import type { VbenFormProps } from '@vben-core/form-ui';
import type { Ref } from 'vue';
import type { import type {
VxeGridListeners, VxeGridListeners,
VxeGridPropTypes, VxeGridPropTypes,
@@ -8,6 +5,12 @@ import type {
VxeUIExport, VxeUIExport,
} from 'vxe-table'; } from 'vxe-table';
import type { Ref } from 'vue';
import type { ClassType, DeepPartial } from '@vben/types';
import type { VbenFormProps } from '@vben-core/form-ui';
import type { VxeGridApi } from './api'; import type { VxeGridApi } from './api';
import { useVbenForm } from '@vben-core/form-ui'; import { useVbenForm } from '@vben-core/form-ui';
@@ -28,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 {
/** /**
* 标题 * 标题
@@ -61,13 +68,17 @@ export interface VxeGridProps {
* 显示搜索表单 * 显示搜索表单
*/ */
showSearchForm?: boolean; showSearchForm?: boolean;
/**
* 搜索表单与表格主体之间的分隔条
*/
separator?: boolean | SeparatorOptions;
} }
export type ExtendedVxeGridApi = { export type ExtendedVxeGridApi = VxeGridApi & {
useStore: <T = NoInfer<VxeGridProps>>( useStore: <T = NoInfer<VxeGridProps>>(
selector?: (state: NoInfer<VxeGridProps>) => T, selector?: (state: NoInfer<VxeGridProps>) => T,
) => Readonly<Ref<T>>; ) => Readonly<Ref<T>>;
} & VxeGridApi; };
export interface SetupVxeTable { export interface SetupVxeTable {
configVxeTable: (ui: VxeUIExport) => void; configVxeTable: (ui: VxeUIExport) => void;

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({
@@ -380,7 +406,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>
@@ -409,6 +446,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

@@ -12,20 +12,55 @@ export function getPopupContainer(node?: HTMLElement): HTMLElement {
/** /**
* VxeTable专用弹窗层 * VxeTable专用弹窗层
* 解决问题: https://gitee.com/dapppp/ruoyi-plus-vben5/issues/IB1DM3 * 解决问题: https://gitee.com/dapppp/ruoyi-plus-vben5/issues/IB1DM3
* @param _node 触发的元素 * @param node 触发的元素
* @param tableId 表格ID用于区分不同表格可选
* @returns 挂载节点 * @returns 挂载节点
*/ */
export function getVxePopupContainer(_node?: HTMLElement): HTMLElement { export function getVxePopupContainer(
node?: HTMLElement,
tableId?: string,
): HTMLElement {
if (!node) return document.body;
// 检查是否在固定列内
const isInFixedColumn =
node.closest('.vxe-table--fixed-wrapper') ||
node.closest('.vxe-table--fixed-left-wrapper') ||
node.closest('.vxe-table--fixed-right-wrapper');
// 如果在固定列内,则挂载到固定列容器
if (isInFixedColumn) {
// 优先查找表格容器及父级容器
const tableContainer =
// 查找通用固定列容器
node.closest('.vxe-table--fixed-wrapper') ||
// 查找固定列容器(左侧固定列)
node.closest('.vxe-table--fixed-left-wrapper') ||
// 查找固定列容器(右侧固定列)
node.closest('.vxe-table--fixed-right-wrapper');
// 如果指定了tableId可以查找特定ID的表格
if (tableId && tableContainer) {
const specificTable = tableContainer.closest(
`[data-table-id="${tableId}"]`,
);
if (specificTable) {
return specificTable as HTMLElement;
}
}
return tableContainer as HTMLElement;
}
/** /**
* 需要区分是否为固定列情况 如果为固定列返回parent会导致展开宽度不正常 * 设置行高度需要特殊处理
* 如果是固定列的情况直接返回body 但是这样不会跟随滚动(个人认为这属于极限场景)
* 如果有更好的办法解决 请告知
*/ */
// if (_node?.closest('td.fixed--width')) { const fixedHeightElement = node.closest('td.col--cs-height');
// return document.body; if (fixedHeightElement) {
// } // 默认为hidden 显示异常
/** (fixedHeightElement as HTMLTableCellElement).style.overflow = 'visible';
* 兼容以前代码 先返回body 这样会造成无法跟随滚动 }
*/
return _node?.parentElement ?? document.body; // 兜底方案:使用元素的父节点或文档体
return (node.parentNode as HTMLElement) || document.body;
} }

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

@@ -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,14 +46,14 @@ 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
'@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
@@ -88,17 +88,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
@@ -146,9 +146,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
@@ -162,12 +162,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
@@ -179,12 +179,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