Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
commit
77c16cd8e2
|
@ -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);
|
||||
|
@ -224,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,
|
||||
|
|
|
@ -13,6 +13,7 @@ export function useModalDraggable(
|
|||
targetRef: Ref<HTMLElement | undefined>,
|
||||
dragRef: Ref<HTMLElement | undefined>,
|
||||
draggable: ComputedRef<boolean>,
|
||||
containerSelector?: ComputedRef<string | undefined>,
|
||||
) {
|
||||
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;
|
||||
|
|
|
@ -23,6 +23,7 @@ const props = withDefaults(defineProps<TreeProps>(), {
|
|||
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<Recordable<any>>) {
|
||||
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<Recordable<any>>) {
|
||||
return props.disabled || get(item.value, props.disabledField);
|
||||
}
|
||||
|
||||
function onToggle(item: FlattenedItem<Recordable<any>>) {
|
||||
emits('expand', item);
|
||||
}
|
||||
function onSelect(item: FlattenedItem<Recordable<any>>, 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({
|
|||
</div>
|
||||
<Checkbox
|
||||
v-if="multiple"
|
||||
:checked="isSelected"
|
||||
:disabled="disabled"
|
||||
:indeterminate="isIndeterminate"
|
||||
:checked="isSelected && !isNodeDisabled(item)"
|
||||
:disabled="isNodeDisabled(item)"
|
||||
:indeterminate="isIndeterminate && !isNodeDisabled(item)"
|
||||
@click="
|
||||
() => {
|
||||
!disabled && handleSelect();
|
||||
// onSelect(item, !isSelected);
|
||||
(event: MouseEvent) => {
|
||||
if (isNodeDisabled(item)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
handleSelect();
|
||||
}
|
||||
"
|
||||
/>
|
||||
<div
|
||||
class="flex items-center gap-1 pl-2"
|
||||
@click="
|
||||
(_event) => {
|
||||
// $event.stopPropagation();
|
||||
// $event.preventDefault();
|
||||
!disabled && handleSelect();
|
||||
// onSelect(item, !isSelected);
|
||||
(event: MouseEvent) => {
|
||||
if (isNodeDisabled(item)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
handleSelect();
|
||||
}
|
||||
"
|
||||
>
|
||||
|
|
|
@ -22,6 +22,8 @@ export interface TreeProps {
|
|||
defaultValue?: Arrayable<number | string>;
|
||||
/** 禁用 */
|
||||
disabled?: boolean;
|
||||
/** 禁用字段名 */
|
||||
disabledField?: string;
|
||||
/** 自定义节点类名 */
|
||||
getNodeClass?: (item: FlattenedItem<Recordable<any>>) => string;
|
||||
iconField?: string;
|
||||
|
|
|
@ -76,6 +76,12 @@ const keyword = ref('');
|
|||
const keywordDebounce = refDebounced(keyword, 300);
|
||||
const innerIcons = ref<string[]>([]);
|
||||
|
||||
/* 当检索关键词变化时,重置分页 */
|
||||
watch(keywordDebounce, () => {
|
||||
currentPage.value = 1;
|
||||
setCurrentPage(1);
|
||||
});
|
||||
|
||||
watchDebounced(
|
||||
() => props.prefix,
|
||||
async (prefix) => {
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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<MaybeElementRef>,
|
||||
refElement: Arrayable<MaybeElementRef> | Ref<HTMLElement[] | null>,
|
||||
delay: (() => number) | HoverDelayOptions | number = DEFAULT_LEAVE_DELAY,
|
||||
) {
|
||||
// 兼容旧版本API
|
||||
|
@ -38,20 +38,58 @@ export function useHoverToggle(
|
|||
...delay,
|
||||
};
|
||||
|
||||
const isHovers: Array<Ref<boolean>> = [];
|
||||
const value = ref(false);
|
||||
const enterTimer = ref<ReturnType<typeof setTimeout> | undefined>();
|
||||
const leaveTimer = ref<ReturnType<typeof setTimeout> | 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<ReturnType<typeof effectScope>[]>([]);
|
||||
|
||||
// 使用计算属性包装 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<Array<Ref<boolean>>>([]);
|
||||
|
||||
// 更新 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];
|
||||
|
|
|
@ -62,21 +62,23 @@ const { authPanelCenter, authPanelLeft, authPanelRight, isDark } =
|
|||
</template>
|
||||
</AuthenticationFormView>
|
||||
|
||||
<!-- 头部 Logo 和应用名称 -->
|
||||
<div
|
||||
v-if="logo || appName"
|
||||
class="absolute left-0 top-0 z-10 flex flex-1"
|
||||
@click="clickLogo"
|
||||
>
|
||||
<slot name="logo">
|
||||
<!-- 头部 Logo 和应用名称 -->
|
||||
<div
|
||||
class="text-foreground lg:text-foreground ml-4 mt-4 flex flex-1 items-center sm:left-6 sm:top-6"
|
||||
v-if="logo || appName"
|
||||
class="absolute left-0 top-0 z-10 flex flex-1"
|
||||
@click="clickLogo"
|
||||
>
|
||||
<img v-if="logo" :alt="appName" :src="logo" class="mr-2" width="42" />
|
||||
<p v-if="appName" class="m-0 text-xl font-medium">
|
||||
{{ appName }}
|
||||
</p>
|
||||
<div
|
||||
class="text-foreground lg:text-foreground ml-4 mt-4 flex flex-1 items-center sm:left-6 sm:top-6"
|
||||
>
|
||||
<img v-if="logo" :alt="appName" :src="logo" class="mr-2" width="42" />
|
||||
<p v-if="appName" class="m-0 text-xl font-medium">
|
||||
{{ appName }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</slot>
|
||||
|
||||
<!-- 系统介绍 -->
|
||||
<div v-if="!authPanelCenter" class="relative hidden w-0 flex-1 lg:block">
|
||||
|
|
Loading…
Reference in New Issue