Merge remote-tracking branch 'origin/feature/v1.3_主agent_rxd' into test

This commit is contained in:
rd
2025-08-29 09:59:00 +08:00
5 changed files with 75 additions and 37 deletions

View File

@ -1,15 +1,14 @@
<script lang="tsx"> <script lang="tsx">
import { message as antdMessage, Tooltip } from 'ant-design-vue'; import { message as antdMessage } from 'ant-design-vue';
import { BubbleList } from '@/components/xt-chat/xt-bubble'; import { BubbleList } from '@/components/xt-chat/xt-bubble';
import SenderInput from './components/sender-input/index.vue'; import SenderInput from './components/sender-input/index.vue';
import RightView from './components/right-view/index.vue'; import RightView from './components/right-view/index.vue';
import { useRoute } from 'vue-router';
import { useChatStore } from '@/stores/modules/chat'; import { useChatStore } from '@/stores/modules/chat';
import { getConversationList } from '@/api/all/chat'; import { getConversationList } from '@/api/all/chat';
import querySSE from '@/utils/querySSE'; import querySSE from '@/utils/querySSE';
import useChatHandler from './useChatHandler'; import useChatHandler from './useChatHandler';
import { QUESTION_ROLE, LOADING_ROLE, REMOTE_ROLE } from './constants'; import { QUESTION_ROLE, LOADING_ROLE } from './constants';
export default { export default {
props: { props: {
@ -85,6 +84,7 @@ export default {
agent_id: chatStore.agentInfo.agent_id, agent_id: chatStore.agentInfo.agent_id,
}), }),
}); });
} catch (error) { } catch (error) {
console.error('Failed to initialize SSE:', error); console.error('Failed to initialize SSE:', error);
antdMessage.error('初始化连接失败'); antdMessage.error('初始化连接失败');
@ -128,6 +128,7 @@ export default {
role: QUESTION_ROLE, role: QUESTION_ROLE,
content: message, content: message,
}); });
initSse(newVal); initSse(newVal);
} }
}, },

View File

@ -226,15 +226,13 @@ export default function useChatHandler(options: UseChatHandlerOptions): UseChatH
)} )}
</div> </div>
)} )}
{isCollapse && ( <div class="relative thought-chain-item" style={{ display: isCollapse ? 'block' : 'none' }}>
<div class="relative thought-chain-item"> <div class="flex items-center mb-4px">
<div class="flex items-center mb-4px"> <img src={isRulCompleted ? icon1 : icon2} width={13} height={13} class="mr-4px" />
<img src={isRulCompleted ? icon1 : icon2} width={13} height={13} class="mr-4px" /> <div>{node}</div>
<div>{node}</div>
</div>
<div v-html={md.render(output)} class={outputEleClass} />
</div> </div>
)} <div v-html={md.render(output)} class={outputEleClass} />
</div>
{customRender?.()} {customRender?.()}
</> </>
); );

View File

@ -118,7 +118,7 @@ export interface BubbleProps<ContentType extends BubbleContentType = string>
*/ */
export interface BubbleRef { export interface BubbleRef {
/** 气泡组件的原生DOM元素 */ /** 气泡组件的原生DOM元素 */
nativeElement: HTMLElement; bubbleElement: HTMLElement;
/** 中止当前打字效果并立即展示完整内容 */ /** 中止当前打字效果并立即展示完整内容 */
abortTyping: VoidFunction; abortTyping: VoidFunction;
} }
@ -138,7 +138,7 @@ export interface BubbleContextProps {
*/ */
export interface BubbleListRef { export interface BubbleListRef {
/** 气泡列表的原生DOM元素 */ /** 气泡列表的原生DOM元素 */
nativeElement: HTMLDivElement; bubbleElement: HTMLDivElement;
/** 滚动到指定位置的方法 */ /** 滚动到指定位置的方法 */
scrollTo: (info: { scrollTo: (info: {
/** 滚动偏移量 */ /** 滚动偏移量 */
@ -180,8 +180,6 @@ export type RolesType = Record<string, RoleType> | ((bubbleDataP: BubbleDataType
* 定义气泡列表组件的所有可配置属性 * 定义气泡列表组件的所有可配置属性
*/ */
export interface BubbleListProps extends /* @vue-ignore */ HTMLAttributes { export interface BubbleListProps extends /* @vue-ignore */ HTMLAttributes {
/** 组件前缀类名 */
prefixCls?: string;
/** 根元素的自定义类名 */ /** 根元素的自定义类名 */
rootClassName?: string; rootClassName?: string;
/** 气泡数据数组 */ /** 气泡数据数组 */

View File

@ -15,6 +15,8 @@ export default defineComponent({
const props = attrs as unknown as BubbleProps<BubbleContentType> & { style?: any; class?: any }; const props = attrs as unknown as BubbleProps<BubbleContentType> & { style?: any; class?: any };
const content = ref<BubbleContentType>(props.content ?? ''); const content = ref<BubbleContentType>(props.content ?? '');
const bubbleElement = ref<HTMLDivElement>(null);
watch( watch(
() => props.content, () => props.content,
(val) => { (val) => {
@ -103,10 +105,11 @@ export default defineComponent({
expose({ expose({
abortTyping, abortTyping,
bubbleElement,
}); });
return () => ( return () => (
<div class={mergedCls.value} style={{ ...(props.style || {}) }}> <div class={mergedCls.value} style={{ ...(props.style || {}) }} ref={bubbleElement}>
{(slots.avatar || props.avatar) && ( {(slots.avatar || props.avatar) && (
<div class={[`${prefixCls}-avatar`, props.classNames?.avatar]} style={props.styles?.avatar}> <div class={[`${prefixCls}-avatar`, props.classNames?.avatar]} style={props.styles?.avatar}>
{avatarNode.value} {avatarNode.value}

View File

@ -23,9 +23,34 @@ import {
export default defineComponent({ export default defineComponent({
name: 'BubbleList', name: 'BubbleList',
inheritAttrs: false, inheritAttrs: false,
props: {}, // 正确声明 props提供默认值确保 TSX 下默认值生效
setup(_, { attrs, slots, expose }) { props: {
const props = attrs as unknown as BubbleListProps & { class?: any; style?: any }; autoScroll: {
type: Boolean,
default: true,
},
items: {
type: Array as () => BubbleListProps['items'],
required: true,
},
roles: {
type: Object as () => RolesType,
default: () => ({} as RolesType),
},
rootClassName: {
type: String,
default: '',
},
style: {
type: Object as () => Record<string, any>,
default: () => ({}),
},
class: {
type: [String, Array, Object] as unknown as () => any,
default: '',
},
},
setup(props, { attrs, slots, expose }) {
const passThroughAttrs = useAttrs(); const passThroughAttrs = useAttrs();
const TOLERANCE = 1; const TOLERANCE = 1;
@ -63,14 +88,16 @@ export default defineComponent({
// scroll // scroll
const [scrollReachEnd, setScrollReachEnd] = useState(true); const [scrollReachEnd, setScrollReachEnd] = useState(true);
const [updateCount, setUpdateCount] = useState(0); const [updateCount, setUpdateCount] = useState(0);
// 首次挂载后仅自动滚动一次
const didInitialAutoScroll = ref(false);
const onInternalScroll = (e: Event) => { const onInternalScroll = (e: Event) => {
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
setScrollReachEnd(target.scrollHeight - Math.abs(target.scrollTop) - target.clientHeight <= TOLERANCE); setScrollReachEnd(target.scrollHeight - Math.abs(target.scrollTop) - target.clientHeight <= TOLERANCE);
}; };
watch(updateCount, () => { watch([updateCount, scrollReachEnd, listRef], () => {
if ((props.autoScroll ?? true) && unref(listRef) && unref(scrollReachEnd)) { if (props.autoScroll && unref(listRef) && unref(scrollReachEnd)) {
nextTick(() => { nextTick(() => {
unref(listRef)!.scrollTo({ top: unref(listRef)!.scrollHeight }); unref(listRef)!.scrollTo({ top: unref(listRef)!.scrollHeight });
}); });
@ -79,26 +106,24 @@ export default defineComponent({
watch( watch(
() => unref(displayData).length, () => unref(displayData).length,
() => { (newLen, oldLen) => {
if (props.autoScroll ?? true) { if (!props.autoScroll) return;
const lastItemKey = unref(displayData)[unref(displayData).length - 2]?.key; // 首次渲染:当有内容时滚到底部一次
const bubbleInst = unref(bubbleRefs)[lastItemKey!]; if (!didInitialAutoScroll.value && newLen > 0) {
if (bubbleInst) { scrollToBottom('auto');
const { nativeElement } = bubbleInst; didInitialAutoScroll.value = true;
const { top = 0, bottom = 0 } = nativeElement?.getBoundingClientRect() ?? {}; return;
const { top: listTop, bottom: listBottom } = unref(listRef)!.getBoundingClientRect(); }
const isVisible = top < listBottom && bottom > listTop; // 新增内容且当前在底部:继续粘底
if (isVisible) { if (oldLen !== undefined && newLen > (oldLen ?? 0) && unref(scrollReachEnd)) {
setUpdateCount(unref(updateCount) + 1); scrollToBottom();
setScrollReachEnd(true);
}
}
} }
}, },
{ immediate: true },
); );
const onBubbleUpdate = useEventCallback<void>(() => { const onBubbleUpdate = useEventCallback<void>(() => {
if (props.autoScroll ?? true) setUpdateCount(unref(updateCount) + 1); if (props.autoScroll) setUpdateCount(unref(updateCount) + 1);
}); });
const context = computed(() => ({ onUpdate: onBubbleUpdate })); const context = computed(() => ({ onUpdate: onBubbleUpdate }));
@ -106,6 +131,18 @@ export default defineComponent({
const abortTypingByKey = (key: string | number) => { const abortTypingByKey = (key: string | number) => {
bubbleRefs.value[key]?.abortTyping?.(); bubbleRefs.value[key]?.abortTyping?.();
}; };
// 通用:滚动到底部
const scrollToBottom = (behavior: ScrollBehavior = 'smooth') => {
nextTick(() => {
requestAnimationFrame(() => {
const el = unref(listRef);
if (el) {
el.scrollTo({ top: el.scrollHeight, behavior });
setScrollReachEnd(true);
}
})
});
};
// 对外暴露能力 // 对外暴露能力
expose({ expose({
nativeElement: listRef, nativeElement: listRef,
@ -113,6 +150,7 @@ export default defineComponent({
scrollTo: (info: any) => { scrollTo: (info: any) => {
unref(listRef)?.scrollTo?.(info); unref(listRef)?.scrollTo?.(info);
}, },
scrollToBottom,
}); });
return () => ( return () => (