feat: bubble组件封装

This commit is contained in:
renxiaodong
2025-08-21 00:24:36 +08:00
parent 6e3158cdb4
commit 64621d9add
17 changed files with 911 additions and 5 deletions

View File

@ -0,0 +1,159 @@
<script lang="tsx">
import Bubble from './Bubble.vue';
import BubbleContextProvider from './context';
import useDisplayData from './hooks/useDisplayData';
import useListData from './hooks/useListData';
import useState from '@/hooks/useState';
import pickAttrs from '@/utils/pick-attrs';
import { useEventCallback } from '@/hooks/useEventCallback';
import type { BubbleListProps, BubbleRef, RolesType } from './types';
import {
computed,
defineComponent,
mergeProps,
nextTick,
onWatcherCleanup,
ref,
unref,
useAttrs,
watch,
watchPostEffect,
} from 'vue';
export default defineComponent({
name: 'BubbleList',
inheritAttrs: false,
props: {} as any,
setup(_, { attrs, slots }) {
const props = attrs as unknown as BubbleListProps & { class?: any; style?: any };
const passThroughAttrs = useAttrs();
const TOLERANCE = 1;
const domProps = computed(() => pickAttrs(mergeProps(props as any, passThroughAttrs)) as any);
const items = ref(props.items);
const roles = ref(props.roles as RolesType);
watch(
() => props.items,
(val) => (items.value = val),
{ immediate: true },
);
watch(
() => props.roles,
(val) => (roles.value = val as RolesType),
{ immediate: true },
);
const listRef = ref<HTMLDivElement | null>(null);
const bubbleRefs = ref<Record<string | number, BubbleRef>>({});
const prefixCls = 'xt-bubble';
const listPrefixCls = `${prefixCls}-list`;
const [initialized, setInitialized] = useState(false);
watchPostEffect(() => {
setInitialized(true);
onWatcherCleanup(() => setInitialized(false));
});
// data
const mergedData = useListData(items as any, roles as any);
const [displayData, onTypingComplete] = useDisplayData(mergedData);
// scroll
const [scrollReachEnd, setScrollReachEnd] = useState(true);
const [updateCount, setUpdateCount] = useState(0);
const onInternalScroll = (e: Event) => {
const target = e.target as HTMLElement;
setScrollReachEnd(target.scrollHeight - Math.abs(target.scrollTop) - target.clientHeight <= TOLERANCE);
};
watch(updateCount, () => {
if ((props.autoScroll ?? true) && unref(listRef) && unref(scrollReachEnd)) {
nextTick(() => {
unref(listRef)!.scrollTo({ top: unref(listRef)!.scrollHeight });
});
}
});
watch(
() => unref(displayData).length,
() => {
if (props.autoScroll ?? true) {
const lastItemKey = unref(displayData)[unref(displayData).length - 2]?.key;
const bubbleInst = unref(bubbleRefs)[lastItemKey!];
if (bubbleInst) {
const { nativeElement } = bubbleInst;
const { top = 0, bottom = 0 } = nativeElement?.getBoundingClientRect() ?? {};
const { top: listTop, bottom: listBottom } = unref(listRef)!.getBoundingClientRect();
const isVisible = top < listBottom && bottom > listTop;
if (isVisible) {
setUpdateCount(unref(updateCount) + 1);
setScrollReachEnd(true);
}
}
}
},
);
const onBubbleUpdate = useEventCallback<void>(() => {
if (props.autoScroll ?? true) setUpdateCount(unref(updateCount) + 1);
});
const context = computed(() => ({ onUpdate: onBubbleUpdate }));
return () => (
<BubbleContextProvider value={context.value}>
<div
{...domProps.value}
class={[
listPrefixCls,
props.rootClassName,
props.class,
{ [`${listPrefixCls}-reach-end`]: unref(scrollReachEnd) },
]}
style={props.style as any}
ref={listRef}
onScroll={onInternalScroll}
>
{unref(displayData).map(({ key, onTypingComplete: onTypingCompleteBubble, ...bubble }: any) => (
<Bubble
{...bubble}
avatar={slots.avatar ? () => slots.avatar?.({ item: { key, ...bubble } }) : (bubble as any).avatar}
header={slots.header?.({ item: { key, ...bubble } }) ?? (bubble as any).header}
footer={slots.footer?.({ item: { key, ...bubble } }) ?? (bubble as any).footer}
loadingRender={
slots.loading ? () => slots.loading({ item: { key, ...bubble } }) : (bubble as any).loadingRender
}
content={slots.message?.({ item: { key, ...bubble } }) ?? (bubble as any).content}
key={key}
ref={(node: any) => {
if (node) {
bubbleRefs.value[key] = node as BubbleRef;
} else {
delete bubbleRefs.value[key];
}
}}
typing={unref(initialized) ? (bubble as any).typing : false}
onTypingComplete={() => {
onTypingCompleteBubble?.();
onTypingComplete(key);
}}
/>
))}
</div>
</BubbleContextProvider>
);
},
});
</script>
<style scoped lang="scss">
@import './style.scss';
</style>
<!--
* @Author: RenXiaoDong
* @Date: 2025-08-20 22:39:35
-->