feat: 长文本组件的对接调试

This commit is contained in:
2026-05-20 15:26:52 +08:00
parent dd7b41d1ad
commit d087ee6b35
6 changed files with 316 additions and 63 deletions

View File

@@ -8,8 +8,19 @@
<!-- 滚动区域 -->
<scroll-view class="flex-full overflow-hidden chat-scroll" scroll-y :scroll-into-view="scrollIntoViewId" scroll-with-animation @scroll="onScroll" @touchstart="onTouchStart" @touchend="onTouchEnd" @touchcancel="onTouchEnd">
<view class="pt-12 px-12 pb-24 border-box">
<!-- 内容支持markdown -->
<ChatMarkdown :text="answerText" />
<template v-for="section in renderSections" :key="section.contentKey">
<view v-if="section.contentKey === 'tag'" class="long-answer-tag">
{{ section.contentValue }}
</view>
<view v-else-if="section.contentKey === 'title'" class="long-answer-title">
{{ section.contentValue }}
</view>
<ChatMarkdown v-else-if="section.contentKey === 'content'" :text="section.contentValue" />
<view v-else-if="section.parsedValue !== null" class="long-answer-block">
<ChatMarkdown :text="toMarkdownText(section.parsedValue)" />
</view>
<ChatMarkdown v-else :text="section.contentValue" />
</template>
<!-- 底部锚点必须存在 -->
<view id="bottom-anchor"></view>
@@ -21,9 +32,13 @@
<script setup>
import TopNavBar from "@/components/TopNavBar/index.vue";
import ChatMarkdown from "../ChatMarkdown/index.vue";
import { defineProps, ref, nextTick } from "vue";
import { defineProps, ref, nextTick, computed } from "vue";
import { onLoad, onUnload } from "@dcloudio/uni-app";
import StreamManager from "@/utils/StreamManager.js";
import {
getLongTextSections,
getLongTextValue,
} from "@/utils/longTextCard";
const props = defineProps({
answerText: {
@@ -34,9 +49,36 @@ const props = defineProps({
const answerText = ref(props.answerText || "");
const title = ref("");
const longTextData = ref(null);
let unsubscribe = null;
const toMarkdownText = (value) => {
if (value === undefined || value === null) return "";
if (Array.isArray(value)) {
return value.map((item) => toMarkdownText(item)).filter(Boolean).join("\n\n");
}
if (typeof value === "object") {
try {
return JSON.stringify(value, null, 2);
} catch (e) {
return String(value);
}
}
return String(value);
};
const renderSections = computed(() => {
const data = longTextData.value;
if (data && data.values) {
return getLongTextSections(data).filter((section) => section.contentValue);
}
return answerText.value
? [{ contentKey: "content", contentValue: answerText.value }]
: [];
});
/** ✅ scroll-into-view 控制 */
const scrollIntoViewId = ref("");
@@ -147,9 +189,12 @@ onLoad(({ message = "", streamId = "", finished = "0" }) => {
// ✅ 流式数据
unsubscribe = StreamManager.subscribe(
streamId,
(text = "", finished = false) => {
(text = "", finished = false, payload = null) => {
answerText.value = text || "";
title.value = computeTitle(answerText.value);
longTextData.value = payload || null;
title.value = computeTitle(
getLongTextValue(longTextData.value, "title") || answerText.value
);
nextTick(() => {
// 每次接收数据都重新测量高度content size 可能变化,比如加载图)
@@ -169,6 +214,7 @@ onLoad(({ message = "", streamId = "", finished = "0" }) => {
} else {
// ✅ 非流式
answerText.value = decodeURIComponent(message || "");
longTextData.value = null;
title.value = computeTitle(answerText.value);
nextTick(() => {
@@ -190,6 +236,27 @@ onUnload(() => {
});
</script>
<style scoped>
</style>
<style scoped lang="scss">
.long-answer-tag {
display: inline-flex;
width: fit-content;
margin-bottom: 8px;
padding: 3px 8px;
border-radius: 12px;
border: 1px solid rgba($theme-color-500, 0.2);
background: rgba($theme-color-500, 0.08);
color: $theme-color-500;
font-size: 12px;
line-height: 18px;
}
.long-answer-title {
margin-bottom: 12px;
color: #111827;
font-size: 20px;
font-weight: 600;
line-height: 28px;
}
.long-answer-block {
margin-bottom: 12px;
}
</style>

View File

@@ -76,8 +76,7 @@
>
<AnswerComponent
v-if="item.componentName === CompName.longTextCard"
:text="item.componentMsg || item.msg"
:title="item.title"
:longTextData="item.longTextData"
:finish="item.finish"
/>
<QuickBookingComponent
@@ -258,6 +257,10 @@ import {
} from "@/request/api/ConversationApi";
import WebSocketManager from "@/utils/WebSocketManager";
import { IdUtils } from "@/utils";
import {
appendLongTextChunk,
createLongTextData,
} from "@/utils/longTextCard";
import { checkToken } from "@/hooks/useGoLogin";
import { useAppStore } from "@/store";
import { getAccessToken } from "@/constant/token";
@@ -781,7 +784,7 @@ const handleWebSocketMessage = (data) => {
messageId: currentSessionMessageId,
replyMessageId: data.replyMessageId || "",
componentName: "",
title: "",
longTextData: null,
finish: false,
};
chatMsgList.value.push(aiMsg);
@@ -803,7 +806,7 @@ const handleWebSocketMessage = (data) => {
messageId: currentSessionMessageId,
replyMessageId: data.replyMessageId || "",
componentName: "",
title: "",
longTextData: null,
finish: false,
};
chatMsgList.value.push(aiMsg);
@@ -842,13 +845,30 @@ const handleWebSocketMessage = (data) => {
if (data.componentName) {
aiItem.componentName = data.componentName;
if (data.componentName === CompName.longTextCard) {
aiItem.longTextData = aiItem.longTextData || createLongTextData();
if (aiItem.msg && aiItem.msg.length > 0) {
aiItem.componentMsg = (aiItem.componentMsg || "") + aiItem.msg;
if (!aiItem.isLoading) {
aiItem.componentMsg = (aiItem.componentMsg || "") + aiItem.msg;
}
aiItem.msg = "";
}
}
}
const isLongText =
aiItem.componentName === CompName.longTextCard ||
data.componentName === CompName.longTextCard;
if (isLongText && data.contentKey) {
aiItem.longTextData = aiItem.longTextData || createLongTextData();
appendLongTextChunk(aiItem.longTextData, data);
if (aiItem.isLoading) {
aiItem.msg = "";
aiItem.isLoading = false;
}
nextTick(() => scrollToBottom());
}
// 确保消息内容是字符串类型
if (data.content && typeof data.content !== "string") {
try {
@@ -861,9 +881,6 @@ const handleWebSocketMessage = (data) => {
// 直接拼接内容到对应 AI 消息
if (data.content) {
// 如果该条消息属于 longTextCard使用 componentMsg 存储内容并保持 ChatCardAI 的 text 为空
const isLongText =
aiItem.componentName === CompName.longTextCard ||
data.componentName === CompName.longTextCard;
if (isLongText) {
if (aiItem.isLoading) {
aiItem.componentMsg = (aiItem.componentMsg || "") + data.content;
@@ -900,7 +917,6 @@ const handleWebSocketMessage = (data) => {
// 处理组件调用
if (data.componentName) {
chatMsgList.value[aiMsgIndex].title = data.content;
chatMsgList.value[aiMsgIndex].componentName = data.componentName;
}
@@ -1173,7 +1189,7 @@ const sendChat = async (message, isInstruct = false) => {
messageId: currentSessionMessageId,
replyMessageId: "",
componentName: "",
title: "",
longTextData: null,
finish: false,
};
chatMsgList.value.push(aiMsg);