From 946f91f387bcfc4c6551e56800adf92c1eb8fdae Mon Sep 17 00:00:00 2001 From: yuhengshen Date: Tue, 24 Jun 2025 17:05:59 +0800 Subject: [PATCH 01/14] feat: optimize modal dragging range(#6414) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 当弹窗指定了容器时,拖拽将被限制在容器范围内 --- .../@core/ui-kit/popup-ui/src/modal/modal.vue | 12 ++++--- .../popup-ui/src/modal/use-modal-draggable.ts | 33 ++++++++++++++----- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/packages/@core/ui-kit/popup-ui/src/modal/modal.vue b/packages/@core/ui-kit/popup-ui/src/modal/modal.vue index e9873ff76..706bb6068 100644 --- a/packages/@core/ui-kit/popup-ui/src/modal/modal.vue +++ b/packages/@core/ui-kit/popup-ui/src/modal/modal.vue @@ -105,10 +105,17 @@ const shouldDraggable = computed( () => draggable.value && !shouldFullscreen.value && header.value, ); +const getAppendTo = computed(() => { + return appendToMain.value + ? `#${ELEMENT_ID_MAIN_CONTENT}>div:not(.absolute)>div` + : undefined; +}); + const { dragging, transform } = useModalDraggable( dialogRef, headerRef, shouldDraggable, + getAppendTo, ); const firstOpened = ref(false); @@ -198,11 +205,6 @@ function handleFocusOutside(e: Event) { e.preventDefault(); e.stopPropagation(); } -const getAppendTo = computed(() => { - return appendToMain.value - ? `#${ELEMENT_ID_MAIN_CONTENT}>div:not(.absolute)>div` - : undefined; -}); const getForceMount = computed(() => { return !unref(destroyOnClose) && unref(firstOpened); diff --git a/packages/@core/ui-kit/popup-ui/src/modal/use-modal-draggable.ts b/packages/@core/ui-kit/popup-ui/src/modal/use-modal-draggable.ts index 0df53948b..8237b011c 100644 --- a/packages/@core/ui-kit/popup-ui/src/modal/use-modal-draggable.ts +++ b/packages/@core/ui-kit/popup-ui/src/modal/use-modal-draggable.ts @@ -13,6 +13,7 @@ export function useModalDraggable( targetRef: Ref, dragRef: Ref, draggable: ComputedRef, + containerSelector?: ComputedRef, ) { const transform = reactive({ offsetX: 0, @@ -30,20 +31,36 @@ export function useModalDraggable( } const targetRect = targetRef.value.getBoundingClientRect(); - const { offsetX, offsetY } = transform; const targetLeft = targetRect.left; const targetTop = targetRect.top; const targetWidth = targetRect.width; const targetHeight = targetRect.height; - const docElement = document.documentElement; - const clientWidth = docElement.clientWidth; - const clientHeight = docElement.clientHeight; - const minLeft = -targetLeft + offsetX; - const minTop = -targetTop + offsetY; - const maxLeft = clientWidth - targetLeft - targetWidth + offsetX; - const maxTop = clientHeight - targetTop - targetHeight + offsetY; + let containerRect: DOMRect | null = null; + + if (containerSelector?.value) { + const container = document.querySelector(containerSelector.value); + if (container) { + containerRect = container.getBoundingClientRect(); + } + } + + let maxLeft, maxTop, minLeft, minTop; + if (containerRect) { + minLeft = containerRect.left - targetLeft + offsetX; + maxLeft = containerRect.right - targetLeft - targetWidth + offsetX; + minTop = containerRect.top - targetTop + offsetY; + maxTop = containerRect.bottom - targetTop - targetHeight + offsetY; + } else { + const docElement = document.documentElement; + const clientWidth = docElement.clientWidth; + const clientHeight = docElement.clientHeight; + minLeft = -targetLeft + offsetX; + minTop = -targetTop + offsetY; + maxLeft = clientWidth - targetLeft - targetWidth + offsetX; + maxTop = clientHeight - targetTop - targetHeight + offsetY; + } const onMousemove = (e: MouseEvent) => { let moveX = offsetX + e.clientX - downX; From 2f7d1f009d09c424e442e9bd998d7a6683cc9dd8 Mon Sep 17 00:00:00 2001 From: yuhengshen Date: Tue, 24 Jun 2025 23:41:54 +0800 Subject: [PATCH 02/14] =?UTF-8?q?fix:=20=E5=85=A8=E5=B1=8F=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E4=B8=8B=E5=BC=B9=E7=AA=97=E5=9C=86=E8=A7=92=E4=BC=98?= =?UTF-8?q?=E5=8C=96=20(#6413)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/@core/ui-kit/popup-ui/src/modal/modal.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/@core/ui-kit/popup-ui/src/modal/modal.vue b/packages/@core/ui-kit/popup-ui/src/modal/modal.vue index 706bb6068..b3fdf3fb4 100644 --- a/packages/@core/ui-kit/popup-ui/src/modal/modal.vue +++ b/packages/@core/ui-kit/popup-ui/src/modal/modal.vue @@ -226,7 +226,8 @@ function handleClosed() { :append-to="getAppendTo" :class=" cn( - 'left-0 right-0 top-[10vh] mx-auto flex max-h-[80%] w-[520px] flex-col p-0 sm:rounded-[var(--radius)]', + 'left-0 right-0 top-[10vh] mx-auto flex max-h-[80%] w-[520px] flex-col p-0', + shouldFullscreen ? 'sm:rounded-none' : 'sm:rounded-[var(--radius)]', modalClass, { 'border-border border': bordered, From e7fd0e3b6a3d5512b374fdd4fe0547702f7f7854 Mon Sep 17 00:00:00 2001 From: broBinChen <139344558+broBinChen@users.noreply.github.com> Date: Fri, 27 Jun 2025 19:08:41 +0800 Subject: [PATCH 03/14] =?UTF-8?q?feat(hooks):=20useHoverToggle=E7=9A=84?= =?UTF-8?q?=E5=85=A5=E5=8F=82refElement=E6=94=AF=E6=8C=81=E4=BC=A0?= =?UTF-8?q?=E5=85=A5=E5=93=8D=E5=BA=94=E5=BC=8F=E6=95=B0=E7=BB=84=20(#6333?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(hooks): useHoverToggle的入参refElement支持传入响应式数组 * feat(hooks): 1、增加 useHoverToggle 中 refElement 参数关于传入响应式数组的注释说明。 2、修改 watch 监听深度,仅需浅层监听 refs 变化。 3、使用 effectScope 管理 useElementHover 实例,避免 refs 变化时事件监听器累积导致的内存泄漏问题 * feat(hooks): 在useHoverToggle中增强 updateHovers 的边界处理,优化watch方案,只监听元素数量变化而不是整个数组变化,避免过度依赖收集 --------- Co-authored-by: xiaobin --- .../effects/hooks/src/use-hover-toggle.ts | 74 +++++++++++++++---- 1 file changed, 58 insertions(+), 16 deletions(-) diff --git a/packages/effects/hooks/src/use-hover-toggle.ts b/packages/effects/hooks/src/use-hover-toggle.ts index 8b1addeb9..0bed41dd3 100644 --- a/packages/effects/hooks/src/use-hover-toggle.ts +++ b/packages/effects/hooks/src/use-hover-toggle.ts @@ -2,7 +2,7 @@ import type { Arrayable, MaybeElementRef } from '@vueuse/core'; import type { Ref } from 'vue'; -import { computed, onUnmounted, ref, unref, watch } from 'vue'; +import { computed, effectScope, onUnmounted, ref, unref, watch } from 'vue'; import { isFunction } from '@vben/utils'; @@ -20,12 +20,12 @@ const DEFAULT_ENTER_DELAY = 0; // 鼠标进入延迟时间,默认为 0(立 /** * 监测鼠标是否在元素内部,如果在元素内部则返回 true,否则返回 false - * @param refElement 所有需要检测的元素。如果提供了一个数组,那么鼠标在任何一个元素内部都会返回 true + * @param refElement 所有需要检测的元素。支持单个元素、元素数组或响应式引用的元素数组。如果鼠标在任何一个元素内部都会返回 true * @param delay 延迟更新状态的时间,可以是数字或包含进入/离开延迟的配置对象 * @returns 返回一个数组,第一个元素是一个 ref,表示鼠标是否在元素内部,第二个元素是一个控制器,可以通过 enable 和 disable 方法来控制监听器的启用和禁用 */ export function useHoverToggle( - refElement: Arrayable, + refElement: Arrayable | Ref, delay: (() => number) | HoverDelayOptions | number = DEFAULT_LEAVE_DELAY, ) { // 兼容旧版本API @@ -38,20 +38,58 @@ export function useHoverToggle( ...delay, }; - const isHovers: Array> = []; const value = ref(false); const enterTimer = ref | undefined>(); const leaveTimer = ref | undefined>(); - const refs = Array.isArray(refElement) ? refElement : [refElement]; - refs.forEach((refEle) => { - const eleRef = computed(() => { - const ele = unref(refEle); - return ele instanceof Element ? ele : (ele?.$el as Element); - }); - const isHover = useElementHover(eleRef); - isHovers.push(isHover); + const hoverScopes = ref[]>([]); + + // 使用计算属性包装 refElement,使其响应式变化 + const refs = computed(() => { + const raw = unref(refElement); + if (raw === null) return []; + return Array.isArray(raw) ? raw : [raw]; }); - const isOutsideAll = computed(() => isHovers.every((v) => !v.value)); + // 存储所有 hover 状态 + const isHovers = ref>>([]); + + // 更新 hover 监听的函数 + function updateHovers() { + // 停止并清理之前的作用域 + hoverScopes.value.forEach((scope) => scope.stop()); + hoverScopes.value = []; + + isHovers.value = refs.value.map((refEle) => { + if (!refEle) { + return ref(false); + } + const eleRef = computed(() => { + const ele = unref(refEle); + return ele instanceof Element ? ele : (ele?.$el as Element); + }); + + // 为每个元素创建独立的作用域 + const scope = effectScope(); + const hoverRef = scope.run(() => useElementHover(eleRef)) || ref(false); + hoverScopes.value.push(scope); + + return hoverRef; + }); + } + + // 监听元素数量变化,避免过度执行 + const elementsCount = computed(() => { + const raw = unref(refElement); + if (raw === null) return 0; + return Array.isArray(raw) ? raw.length : 1; + }); + + // 初始设置 + updateHovers(); + + // 只在元素数量变化时重新设置监听器 + const stopWatcher = watch(elementsCount, updateHovers, { deep: false }); + + const isOutsideAll = computed(() => isHovers.value.every((v) => !v.value)); function clearTimers() { if (enterTimer.value) { @@ -96,7 +134,7 @@ export function useHoverToggle( } } - const watcher = watch( + const hoverWatcher = watch( isOutsideAll, (val) => { setValueDelay(!val); @@ -106,15 +144,19 @@ export function useHoverToggle( const controller = { enable() { - watcher.resume(); + hoverWatcher.resume(); }, disable() { - watcher.pause(); + hoverWatcher.pause(); }, }; onUnmounted(() => { clearTimers(); + // 停止监听器 + stopWatcher(); + // 停止所有剩余的作用域 + hoverScopes.value.forEach((scope) => scope.stop()); }); return [value, controller] as [typeof value, typeof controller]; From 3230781538495574de6ec18c1adeb69231e42413 Mon Sep 17 00:00:00 2001 From: "CG.gatspy" Date: Fri, 27 Jun 2025 19:09:30 +0800 Subject: [PATCH 04/14] =?UTF-8?q?feat:=20[vben-tree]=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E6=95=B0=E6=8D=AEdisabled=20(#6343)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: [vben-tree]增加数据disabled * Update packages/@core/ui-kit/shadcn-ui/src/ui/tree/tree.vue Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: Jin Mao <50581550+jinmao88@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../ui-kit/shadcn-ui/src/ui/tree/tree.vue | 90 ++++++++++++++----- .../ui-kit/shadcn-ui/src/ui/tree/types.ts | 2 + 2 files changed, 69 insertions(+), 23 deletions(-) diff --git a/packages/@core/ui-kit/shadcn-ui/src/ui/tree/tree.vue b/packages/@core/ui-kit/shadcn-ui/src/ui/tree/tree.vue index 03117b363..339d96541 100644 --- a/packages/@core/ui-kit/shadcn-ui/src/ui/tree/tree.vue +++ b/packages/@core/ui-kit/shadcn-ui/src/ui/tree/tree.vue @@ -23,6 +23,7 @@ const props = withDefaults(defineProps(), { defaultExpandedKeys: () => [], defaultExpandedLevel: 0, disabled: false, + disabledField: 'disabled', expanded: () => [], iconField: 'icon', labelField: 'label', @@ -101,16 +102,37 @@ function updateTreeValue() { if (val === undefined) { treeValue.value = undefined; } else { - treeValue.value = Array.isArray(val) - ? val.map((v) => getItemByValue(v)) - : getItemByValue(val); + if (Array.isArray(val)) { + const filteredValues = val.filter((v) => { + const item = getItemByValue(v); + return item && !get(item, props.disabledField); + }); + treeValue.value = filteredValues.map((v) => getItemByValue(v)); + + if (filteredValues.length !== val.length) { + modelValue.value = filteredValues; + } + } else { + const item = getItemByValue(val); + if (item && !get(item, props.disabledField)) { + treeValue.value = item; + } else { + treeValue.value = undefined; + modelValue.value = undefined; + } + } } } function updateModelValue(val: Arrayable>) { - modelValue.value = Array.isArray(val) - ? val.map((v) => get(v, props.valueField)) - : get(val, props.valueField); + if (Array.isArray(val)) { + const filteredVal = val.filter((v) => !get(v, props.disabledField)); + modelValue.value = filteredVal.map((v) => get(v, props.valueField)); + } else { + if (val && !get(val, props.disabledField)) { + modelValue.value = get(val, props.valueField); + } + } } function expandToLevel(level: number) { @@ -149,10 +171,18 @@ function collapseAll() { expanded.value = []; } +function isNodeDisabled(item: FlattenedItem>) { + return props.disabled || get(item.value, props.disabledField); +} + function onToggle(item: FlattenedItem>) { emits('expand', item); } function onSelect(item: FlattenedItem>, isSelected: boolean) { + if (isNodeDisabled(item)) { + return; + } + if ( !props.checkStrictly && props.multiple && @@ -224,28 +254,34 @@ defineExpose({ :class=" cn('cursor-pointer', getNodeClass?.(item), { 'data-[selected]:bg-accent': !multiple, - 'cursor-not-allowed': disabled, + 'cursor-not-allowed': isNodeDisabled(item), }) " v-bind=" Object.assign(item.bind, { - onfocus: disabled ? 'this.blur()' : undefined, + onfocus: isNodeDisabled(item) ? 'this.blur()' : undefined, + disabled: isNodeDisabled(item), }) " @select=" - (event) => { + (event: any) => { + if (isNodeDisabled(item)) { + event.preventDefault(); + event.stopPropagation(); + return; + } if (event.detail.originalEvent.type === 'click') { event.preventDefault(); } - !disabled && onSelect(item, event.detail.isSelected); + onSelect(item, event.detail.isSelected); } " @toggle=" - (event) => { + (event: any) => { if (event.detail.originalEvent.type === 'click') { event.preventDefault(); } - !disabled && onToggle(item); + !isNodeDisabled(item) && onToggle(item); } " class="tree-node focus:ring-grass8 my-0.5 flex items-center rounded px-2 py-1 outline-none focus:ring-2" @@ -266,24 +302,32 @@ defineExpose({
diff --git a/packages/@core/ui-kit/shadcn-ui/src/ui/tree/types.ts b/packages/@core/ui-kit/shadcn-ui/src/ui/tree/types.ts index 5c394f0da..97b091390 100644 --- a/packages/@core/ui-kit/shadcn-ui/src/ui/tree/types.ts +++ b/packages/@core/ui-kit/shadcn-ui/src/ui/tree/types.ts @@ -22,6 +22,8 @@ export interface TreeProps { defaultValue?: Arrayable; /** 禁用 */ disabled?: boolean; + /** 禁用字段名 */ + disabledField?: string; /** 自定义节点类名 */ getNodeClass?: (item: FlattenedItem>) => string; iconField?: string; From 5c3972196a94336f6ee26bbd0fa00aa79ecb4691 Mon Sep 17 00:00:00 2001 From: Li Kui <90845831+likui628@users.noreply.github.com> Date: Fri, 27 Jun 2025 19:20:25 +0800 Subject: [PATCH 05/14] fix: Add $t import to login expired modal (#6429) closes #6230 --- .../common-ui/src/ui/authentication/login-expired-modal.vue | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/effects/common-ui/src/ui/authentication/login-expired-modal.vue b/packages/effects/common-ui/src/ui/authentication/login-expired-modal.vue index c337c29da..ef5e6a009 100644 --- a/packages/effects/common-ui/src/ui/authentication/login-expired-modal.vue +++ b/packages/effects/common-ui/src/ui/authentication/login-expired-modal.vue @@ -3,6 +3,8 @@ import type { AuthenticationProps } from './types'; import { computed, watch } from 'vue'; +import { $t } from '@vben/locales'; + import { useVbenModal } from '@vben-core/popup-ui'; import { Slot, VbenAvatar } from '@vben-core/shadcn-ui'; From de14908fd3aa60246e60f250385f65b8bb92d46f Mon Sep 17 00:00:00 2001 From: Stephen Chang Date: Fri, 27 Jun 2025 19:21:23 +0800 Subject: [PATCH 06/14] =?UTF-8?q?fix(icon-picker):=20=E8=A7=A3=E5=86=B3ico?= =?UTF-8?q?n-picker=E7=BB=84=E4=BB=B6=E5=88=87=E6=8D=A2=E5=88=86=E9=A1=B5?= =?UTF-8?q?=E5=90=8E=EF=BC=8C=E5=85=B3=E9=94=AE=E8=AF=8D=E6=A3=80=E7=B4=A2?= =?UTF-8?q?=E5=A4=B1=E6=95=88=E9=97=AE=E9=A2=98=20(#6437)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 当icon-picker组件切换分页后,在输入关键词检索,但是分页没有重置,导致检索结果异常 --- .../common-ui/src/components/icon-picker/icon-picker.vue | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/effects/common-ui/src/components/icon-picker/icon-picker.vue b/packages/effects/common-ui/src/components/icon-picker/icon-picker.vue index 51ef88d0b..c81639f9b 100644 --- a/packages/effects/common-ui/src/components/icon-picker/icon-picker.vue +++ b/packages/effects/common-ui/src/components/icon-picker/icon-picker.vue @@ -76,6 +76,12 @@ const keyword = ref(''); const keywordDebounce = refDebounced(keyword, 300); const innerIcons = ref([]); +/* 当检索关键词变化时,重置分页 */ +watch(keywordDebounce, () => { + currentPage.value = 1; + setCurrentPage(1); +}); + watchDebounced( () => props.prefix, async (prefix) => { From b1fb6231136f5f63c1ff590255477f73c618ca25 Mon Sep 17 00:00:00 2001 From: Utopia Date: Fri, 27 Jun 2025 19:23:24 +0800 Subject: [PATCH 07/14] =?UTF-8?q?feat:=20=E4=B8=BA=20auth=20layout=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20slot:=20logo,=20=E6=8F=90=E5=8D=87?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E7=9A=84=E7=81=B5=E6=B4=BB=E6=80=A7=E5=92=8C?= =?UTF-8?q?=E5=8F=AF=E5=A4=8D=E7=94=A8=E6=80=A7=20(#6442)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/authentication/authentication.vue | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/effects/layouts/src/authentication/authentication.vue b/packages/effects/layouts/src/authentication/authentication.vue index cb500de51..e66954278 100644 --- a/packages/effects/layouts/src/authentication/authentication.vue +++ b/packages/effects/layouts/src/authentication/authentication.vue @@ -62,21 +62,23 @@ const { authPanelCenter, authPanelLeft, authPanelRight, isDark } = - -
+ +
- -

- {{ appName }} -

+
+ +

+ {{ appName }} +

+
-
+