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 03117b36..339d9654 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 5c394f0d..97b09139 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; diff --git a/packages/effects/common-ui/package.json b/packages/effects/common-ui/package.json index 7b8f41a7..66f9f21a 100644 --- a/packages/effects/common-ui/package.json +++ b/packages/effects/common-ui/package.json @@ -49,6 +49,7 @@ "@vueuse/core": "catalog:", "@vueuse/integrations": "catalog:", "codemirror": "6.0.1", + "json-bigint": "catalog:", "qrcode": "catalog:", "tippy.js": "catalog:", "vditor": "3.10.9", 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 51ef88d0..c81639f9 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) => { diff --git a/packages/effects/common-ui/src/components/json-viewer/index.vue b/packages/effects/common-ui/src/components/json-viewer/index.vue index aa1d90d3..b69c655e 100644 --- a/packages/effects/common-ui/src/components/json-viewer/index.vue +++ b/packages/effects/common-ui/src/components/json-viewer/index.vue @@ -18,6 +18,9 @@ import { $t } from '@vben/locales'; import { isBoolean } from '@vben-core/shared/utils'; +// @ts-ignore +import JsonBigint from 'json-bigint'; + defineOptions({ name: 'JsonViewer' }); const props = withDefaults(defineProps(), { @@ -68,6 +71,20 @@ function handleClick(event: MouseEvent) { emit('click', event); } +// 支持显示 bigint 数据,如较长的订单号 +const jsonData = computed>(() => { + if (typeof props.value !== 'string') { + return props.value || {}; + } + + try { + return JsonBigint({ storeAsString: true }).parse(props.value); + } catch (error) { + console.error('JSON parse error:', error); + return {}; + } +}); + const bindProps = computed>(() => { const copyable = { copyText: $t('ui.jsonViewer.copy'), @@ -79,6 +96,7 @@ const bindProps = computed>(() => { return { ...props, ...attrs, + value: jsonData.value, onCopied: (event: JsonViewerAction) => emit('copied', event), onKeyclick: (key: string) => emit('keyClick', key), onClick: (event: MouseEvent) => handleClick(event), 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 c337c29d..ef5e6a00 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'; diff --git a/packages/effects/hooks/src/use-hover-toggle.ts b/packages/effects/hooks/src/use-hover-toggle.ts index 8b1addeb..0bed41dd 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 hoverScopes = ref[]>([]); + + // 使用计算属性包装 refElement,使其响应式变化 + const refs = computed(() => { + const raw = unref(refElement); + if (raw === null) return []; + return Array.isArray(raw) ? raw : [raw]; + }); + // 存储所有 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 isHover = useElementHover(eleRef); - isHovers.push(isHover); + } + + // 监听元素数量变化,避免过度执行 + const elementsCount = computed(() => { + const raw = unref(refElement); + if (raw === null) return 0; + return Array.isArray(raw) ? raw.length : 1; }); - const isOutsideAll = computed(() => isHovers.every((v) => !v.value)); + + // 初始设置 + 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]; diff --git a/packages/effects/layouts/src/authentication/authentication.vue b/packages/effects/layouts/src/authentication/authentication.vue index cb500de5..e6695427 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 }} +

+
-
+