diff --git a/src/components/xt-chat/xt-bubble/hooks/useListData.ts b/src/components/xt-chat/xt-bubble/hooks/useListData.ts index 2c59520..7ab47b1 100644 --- a/src/components/xt-chat/xt-bubble/hooks/useListData.ts +++ b/src/components/xt-chat/xt-bubble/hooks/useListData.ts @@ -62,8 +62,8 @@ export default function useListData( */ const listData = computed(() => (items.value || []).map((bubbleData, i) => { - // 生成唯一key,如果没有提供key则使用预设格式 - const mergedKey = bubbleData.key ?? `preset_${i}`; + // 生成唯一key:优先使用传入的 key,其次使用 id,最后回退到预设格式 + const mergedKey = (bubbleData as any).key ?? (bubbleData as any).id ?? `preset_${i}`; return { // 先应用角色配置(作为默认值) diff --git a/src/components/xt-chat/xt-bubble/hooks/useTypedEffect.ts b/src/components/xt-chat/xt-bubble/hooks/useTypedEffect.ts index baf54f6..eaffc0a 100644 --- a/src/components/xt-chat/xt-bubble/hooks/useTypedEffect.ts +++ b/src/components/xt-chat/xt-bubble/hooks/useTypedEffect.ts @@ -34,6 +34,7 @@ const useTypedEffect = ( typingEnabled: Ref, typingStep: Ref, typingInterval: Ref, + abortRef?: Ref, ): [typedContent: Ref, isTyping: Ref] => { // 记录上一次的内容,用于检测内容变化 const [prevContent, setPrevContent] = useState(''); @@ -51,9 +52,12 @@ const useTypedEffect = ( // 更新上一次的内容记录 setPrevContent(content.value); - // 如果未启用打字效果且内容为字符串,直接显示全部内容 + // 如果未启用打字效果且内容为字符串 if (!mergedTypingEnabled.value && isString(content.value)) { - setTypingIndex(content.value.length); + // 若外部触发中止,则保持当前索引,不再自动跳到全文 + if (!(abortRef && abortRef.value)) { + setTypingIndex(content.value.length); + } } else if ( // 如果内容为字符串,且新内容不是以旧内容开头,重置打字索引 isString(content.value) && @@ -68,10 +72,15 @@ const useTypedEffect = ( // 启动打字效果 watch( - [typingIndex, typingEnabled, content], + [typingIndex, typingEnabled, content, abortRef as any], () => { // 只有在启用打字、内容为字符串且未显示完所有内容时才执行 - if (mergedTypingEnabled.value && isString(content.value) && unref(typingIndex) < content.value.length) { + if ( + mergedTypingEnabled.value && + isString(content.value) && + unref(typingIndex) < content.value.length && + !(abortRef && abortRef.value) + ) { // 设置定时器,逐步增加显示字符数 const id = setTimeout(() => { setTypingIndex(unref(typingIndex) + typingStep.value); @@ -88,8 +97,11 @@ const useTypedEffect = ( // 计算当前应该显示的内容 const mergedTypingContent = computed(() => - // 如果启用打字且内容为字符串,显示部分内容;否则显示全部内容 - mergedTypingEnabled.value && isString(content.value) ? content.value.slice(0, unref(typingIndex)) : content.value, + // 如果启用打字且内容为字符串,显示部分内容; + // 或外部中止时,固定在当前索引;否则显示全部内容 + (mergedTypingEnabled.value || (abortRef && abortRef.value)) && isString(content.value) + ? content.value.slice(0, unref(typingIndex)) + : content.value, ); return [ diff --git a/src/components/xt-chat/xt-bubble/style.scss b/src/components/xt-chat/xt-bubble/style.scss index e2ad047..3821521 100644 --- a/src/components/xt-chat/xt-bubble/style.scss +++ b/src/components/xt-chat/xt-bubble/style.scss @@ -1,4 +1,5 @@ .xt-bubble-list { + height: 100%; gap: 8px; display: flex; flex-direction: column; @@ -78,6 +79,31 @@ &-corner { border-radius: 4px; } + + :deep(table) { + border-collapse: collapse; + thead { + tr { + th { + @include cts; + padding: 6px 8px; + border: 1px solid #E6E6E8; + text-align: left; + + } + } + } + tbody { + tr { + td { + @include cts; + border: 1px solid #E6E6E8; + padding: 16px 8px; + text-align: left; + } + } + } + } } .xt-bubble-header { @@ -94,4 +120,5 @@ display: inline-block; min-width: 36px; } + } diff --git a/src/components/xt-chat/xt-bubble/types.ts b/src/components/xt-chat/xt-bubble/types.ts index cd4ab45..333d7f4 100644 --- a/src/components/xt-chat/xt-bubble/types.ts +++ b/src/components/xt-chat/xt-bubble/types.ts @@ -119,6 +119,8 @@ export interface BubbleProps export interface BubbleRef { /** 气泡组件的原生DOM元素 */ nativeElement: HTMLElement; + /** 中止当前打字效果并立即展示完整内容 */ + abortTyping: VoidFunction; } /** diff --git a/src/components/xt-chat/xt-bubble/xt-bubble.vue b/src/components/xt-chat/xt-bubble/xt-bubble.vue index 4d1c640..171c445 100644 --- a/src/components/xt-chat/xt-bubble/xt-bubble.vue +++ b/src/components/xt-chat/xt-bubble/xt-bubble.vue @@ -11,7 +11,7 @@ export default defineComponent({ name: 'Bubble', inheritAttrs: false, props: {} as any, - setup(_, { attrs, slots }) { + setup(_, { attrs, slots, expose }) { const props = attrs as unknown as BubbleProps & { style?: any; class?: any }; const content = ref(props.content ?? ''); @@ -26,7 +26,19 @@ export default defineComponent({ const { onUpdate } = unref(useBubbleContextInject()); const [typingEnabled, typingStep, typingInterval, typingSuffix] = useTypingConfig(() => props.typing); - const [typedContent, isTyping] = useTypedEffect(content as any, typingEnabled, typingStep, typingInterval); + const abortRef = ref(false); + const [typedContent, isTyping] = useTypedEffect( + content as any, + typingEnabled, + typingStep, + typingInterval, + abortRef, + ); + + // 提供中止打字的能力:关闭typing并立即展示完整内容 + const abortTyping = () => { + abortRef.value = true; + }; const triggerTypingCompleteRef = ref(false); watch(typedContent, () => { @@ -97,6 +109,10 @@ export default defineComponent({ return typeof f === 'function' ? f({ content: typedContent.value as any, info, item: props }) : f; }; + expose({ + abortTyping, + }); + return () => (
{(slots.avatar || props.avatar) && ( diff --git a/src/components/xt-chat/xt-bubble/xt-bubbleList.vue b/src/components/xt-chat/xt-bubble/xt-bubbleList.vue index 00676c0..040c12c 100644 --- a/src/components/xt-chat/xt-bubble/xt-bubbleList.vue +++ b/src/components/xt-chat/xt-bubble/xt-bubbleList.vue @@ -24,7 +24,7 @@ export default defineComponent({ name: 'BubbleList', inheritAttrs: false, props: {} as any, - setup(_, { attrs, slots }) { + setup(_, { attrs, slots, expose }) { const props = attrs as unknown as BubbleListProps & { class?: any; style?: any }; const passThroughAttrs = useAttrs(); @@ -48,7 +48,7 @@ export default defineComponent({ const listRef = ref(null); const bubbleRefs = ref>({}); - const listPrefixCls = 'xt-bubble-list' + const listPrefixCls = 'xt-bubble-list'; const [initialized, setInitialized] = useState(false); watchPostEffect(() => { @@ -102,6 +102,19 @@ export default defineComponent({ }); const context = computed(() => ({ onUpdate: onBubbleUpdate })); + // 暴露控制方法 + const abortTypingByKey = (key: string | number) => { + bubbleRefs.value[key]?.abortTyping?.(); + }; + // 对外暴露能力 + expose({ + nativeElement: listRef as any, + abortTypingByKey, + scrollTo: (info: any) => { + unref(listRef)?.scrollTo?.(info); + }, + }); + return () => (
{ @@ -30,12 +29,18 @@ const showSider = computed(() => { const isHomeRoute = computed(() => { return route.name === 'Home'; }); +const showInOnePage = computed(() => { + return isHomeRoute.value; +}); const layoutPageClass = computed(() => { + let result = showInOnePage.value ? 'overflow-hidden' : ''; if (isHomeRoute.value) { - return 'pb-8px pr-8px'; + result += ' pb-8px pr-8px'; + } else { + result += ' pb-24px pr-24px'; } - return 'pb-24px pr-24px'; + return result; }); const siderWidth = computed(() => { diff --git a/src/views/home/components/conversation-detail/index.vue b/src/views/home/components/conversation-detail/index.vue index 3f8aaa8..f8bb252 100644 --- a/src/views/home/components/conversation-detail/index.vue +++ b/src/views/home/components/conversation-detail/index.vue @@ -1,13 +1,15 @@ + + diff --git a/vite.config.ts b/vite.config.ts index 60742a1..3c96a9c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -17,6 +17,10 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => { const setServer = setServerConfig({ env }); return { + define: { + __VUE_PROD_DEVTOOLS__: false, + __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false, + }, css: { preprocessorOptions: { scss: {