import { nextTick, onMounted, onUnmounted, type Ref, ref, h } from 'vue';
import type { Component, VNode } from 'vue';
function throttle<T extends (...args: any[]) => any>(fn: T, delay: number): (...args: Parameters<T>) => void {
let timer: ReturnType<typeof setTimeout> | null = null;
let lastTime = 0;
return function (this: any, ...args: Parameters<T>) {
const now = Date.now();
const elapsed = now - lastTime;
if (elapsed >= delay) {
if (timer) {
clearTimeout(timer);
timer = null;
}
lastTime = now;
fn.apply(this, args);
} else {
if (timer) {
clearTimeout(timer);
}
const remaining = delay - elapsed;
timer = setTimeout(() => {
lastTime = Date.now();
timer = null;
fn.apply(this, args);
}, remaining);
}
};
}
const scrollEnd = (container: Ref<HTMLElement | undefined>) => {
const isLastMessageInBottom = ref(false);
const isScrollByUser = ref(false);
let scrollByUserTimer: ReturnType<typeof setTimeout> | null = null;
const updateIsLastMessageInBottom = () => {
if (container.value) {
isLastMessageInBottom.value =
container.value.scrollTop + container.value.clientHeight + 130 >= container.value.scrollHeight;
}
};
const scrollToBottom = () => {
if (container.value) {
nextTick(() => {
container.value?.scrollTo({
top: container.value.scrollHeight,
behavior: 'smooth',
});
});
}
};
const scrollToBottomWithRetry = (maxRetries = 10, retryDelay = 150) => {
if (!container.value) return;
let retryCount = 0;
let lastScrollHeight = 0;
let stableCount = 0;
const attemptScroll = () => {
if (!container.value) return;
const currentScrollHeight = container.value.scrollHeight;
const scrollTop = container.value.scrollTop;
const clientHeight = container.value.clientHeight;
const isAtBottom = scrollTop + clientHeight + 10 >= currentScrollHeight;
if (currentScrollHeight !== lastScrollHeight) {
lastScrollHeight = currentScrollHeight;
stableCount = 0;
retryCount = 0;
} else {
stableCount++;
}
if (isAtBottom) {
return;
}
if (stableCount >= 2 || retryCount >= 2) {
scrollToBottom();
}
if (!isAtBottom && retryCount < maxRetries) {
retryCount++;
setTimeout(() => {
requestAnimationFrame(() => {
attemptScroll();
});
}, retryDelay);
}
};
setTimeout(() => {
attemptScroll();
updateIsLastMessageInBottom();
}, 50);
};
const autoScrollToBottom = () => {
if (!isLastMessageInBottom.value || isScrollByUser.value) {
return;
}
scrollToBottom();
};
const handleUserScroll = () => {
isScrollByUser.value = true;
if (scrollByUserTimer) {
clearTimeout(scrollByUserTimer);
}
scrollByUserTimer = setTimeout(() => {
isScrollByUser.value = false;
scrollByUserTimer = null;
}, 100);
};
onMounted(() => {
updateIsLastMessageInBottom();
if (container.value) {
container.value.addEventListener('scroll', updateIsLastMessageInBottom, { passive: true });
container.value.addEventListener('wheel', handleUserScroll, { passive: true });
container.value.addEventListener('touchmove', handleUserScroll, { passive: true });
}
});
onUnmounted(() => {
if (container.value) {
container.value.removeEventListener('scroll', updateIsLastMessageInBottom);
container.value.removeEventListener('wheel', handleUserScroll);
container.value.removeEventListener('touchmove', handleUserScroll);
}
});
return {
scrollToBottom,
scrollToBottomWithRetry,
autoScrollToBottom,
isLastMessageInBottom,
updateIsLastMessageInBottom,
};
};
type ISlotProps = any;
export const toSlotFunction = (slot: Component<ISlotProps> | ((props: ISlotProps) => VNode | VNode[]) | undefined) => {
if (!slot) return undefined;
if (typeof slot === 'function' && !('__name' in slot || 'setup' in slot || 'render' in slot)) {
return slot as (props: ISlotProps) => VNode | VNode[];
}
return (props: ISlotProps) => h(slot as Component<ISlotProps>, props);
};
export { scrollEnd, throttle };