feat:优化回答消息的返回方式与交互样式调整

This commit is contained in:
2026-03-27 00:57:22 +08:00
parent b862c061a2
commit 8149a49ff0
3 changed files with 126 additions and 71 deletions

View File

@@ -477,31 +477,62 @@ const handleWebSocketMessage = (data) => {
} }
let aiMsgIndex = -1; let aiMsgIndex = -1;
if (currentSessionMessageId && pendingMap.has(currentSessionMessageId)) { // Prefer matching by replyMessageId if provided
aiMsgIndex = pendingMap.get(currentSessionMessageId); if (data.replyMessageId) {
if (aiMsgIndex >= 0 && aiMsgIndex < chatMsgList.value.length) { // 1) Try to find an existing AI message that already has the same replyMessageId
const item = chatMsgList.value[aiMsgIndex]; for (let i = chatMsgList.value.length - 1; i >= 0; i--) {
if (item && item.msgType === MessageRole.AI && const it = chatMsgList.value[i];
item.replyMessageId.length > 0 && data.replyMessageId && if (it && it.msgType === MessageRole.AI && it.replyMessageId === data.replyMessageId) {
item.replyMessageId !== data.replyMessageId) { aiMsgIndex = i;
// 已经存在对应的AI消息项继续使用 break;
const aiMsg = { }
msgId: `msg_${chatMsgList.value.length}`, }
msgType: MessageRole.AI,
msg: "", // 2) If not found, check pendingMap for currentSessionMessageId
isLoading: false, if (aiMsgIndex === -1 && currentSessionMessageId && pendingMap.has(currentSessionMessageId)) {
messageId: currentSessionMessageId, const idx = pendingMap.get(currentSessionMessageId);
replyMessageId: '', if (idx >= 0 && idx < chatMsgList.value.length) {
componentName: "", const item = chatMsgList.value[idx];
title: "", // If the pending item already has a different non-empty replyMessageId, create a new AI entry
finish: false, if (item && item.msgType === MessageRole.AI && item.replyMessageId && item.replyMessageId !== data.replyMessageId) {
}; const aiMsg = {
chatMsgList.value.push(aiMsg); msgId: `msg_${chatMsgList.value.length}`,
aiMsgIndex = chatMsgList.value.length - 1; msgType: MessageRole.AI,
} msg: "",
isLoading: false,
messageId: currentSessionMessageId,
replyMessageId: data.replyMessageId || '',
componentName: "",
title: "",
finish: false,
};
chatMsgList.value.push(aiMsg);
aiMsgIndex = chatMsgList.value.length - 1;
} else {
// Reuse the pending item
aiMsgIndex = idx;
}
}
}
// 3) If still not found, create a new AI message for this replyMessageId
if (aiMsgIndex === -1) {
const aiMsg = {
msgId: `msg_${chatMsgList.value.length}`,
msgType: MessageRole.AI,
msg: "",
isLoading: false,
messageId: currentSessionMessageId,
replyMessageId: data.replyMessageId || '',
componentName: "",
title: "",
finish: false,
};
chatMsgList.value.push(aiMsg);
aiMsgIndex = chatMsgList.value.length - 1;
} }
} else { } else {
// 向后搜索最近的 AI 消息(回退逻辑) // No replyMessageId: fall back to most recent AI message
for (let i = chatMsgList.value.length - 1; i >= 0; i--) { for (let i = chatMsgList.value.length - 1; i >= 0; i--) {
if (chatMsgList.value[i] && chatMsgList.value[i].msgType === MessageRole.AI) { if (chatMsgList.value[i] && chatMsgList.value[i].msgType === MessageRole.AI) {
aiMsgIndex = i; aiMsgIndex = i;
@@ -572,7 +603,7 @@ const handleWebSocketMessage = (data) => {
} }
// 清理 pendingMap / timeout // 清理 pendingMap / timeout
const ownedMessageId = chatMsgList.value[aiMsgIndex].messageId || msgId; const ownedMessageId = chatMsgList.value[aiMsgIndex].messageId || null;
if (ownedMessageId) { if (ownedMessageId) {
if (pendingTimeouts.has(ownedMessageId)) { if (pendingTimeouts.has(ownedMessageId)) {
clearTimeout(pendingTimeouts.get(ownedMessageId)); clearTimeout(pendingTimeouts.get(ownedMessageId));

View File

@@ -3,7 +3,7 @@
<!-- 占位撑开 --> <!-- 占位撑开 -->
<view class="w-vw"></view> <view class="w-vw"></view>
<view class="flex flex-col p-16 border-box"> <view class="flex flex-col p-16 border-box">
<view class="flex flex-row flex-items-start justify-center"> <view class="flex flex-row flex-items-start flex-justify-start">
<uni-icons class="icon-active" type="fire-filled" size="18" color="opacity" /> <uni-icons class="icon-active" type="fire-filled" size="18" color="opacity" />
<text class="font-size-16 font-500 text-color-900 ml-6">游玩划重点</text> <text class="font-size-16 font-500 text-color-900 ml-6">游玩划重点</text>
</view> </view>
@@ -12,7 +12,7 @@
<ChatMarkdown :key="textKey" :text="processedText" /> <ChatMarkdown :key="textKey" :text="processedText" />
</view> </view>
<!-- 超过3行时显示...提示 --> <!-- 超过3行时显示...提示 -->
<view class="flex flex-row mt-8" v-if="isOverflow" @click="lookDetailAction"> <view class="flex flex-row flex-items-center mt-8" v-if="isOverflow" @click="lookDetailAction">
<text class="font-size-12 font-400 theme-color-500 mr-4">查看详情</text> <text class="font-size-12 font-400 theme-color-500 mr-4">查看详情</text>
<uni-icons class="icon-active" type="right" size="14" color="opacity"></uni-icons> <uni-icons class="icon-active" type="right" size="14" color="opacity"></uni-icons>
</view> </view>
@@ -43,10 +43,28 @@ const props = defineProps({
// 用于强制重新渲染的key // 用于强制重新渲染的key
const textKey = ref(0); const textKey = ref(0);
// 处理文本内容(纯计算,不应有副作用 // 处理文本内容:按行截断以保证预览最多显示三行(更贴近视觉行数
// 点击“查看详情”会跳转到完整页面(不受预览截断影响)。
const PREVIEW_LINES = 3;
const PREVIEW_CHAR_LIMIT = 100; // 作为备用,当没有换行但过长时也会截断
const processedText = computed(() => { const processedText = computed(() => {
if (!props.text) return ""; const txt = props.text ? String(props.text) : "";
return String(props.text); if (!txt) return "";
// 按行分割(保留空行)
const lines = txt.split(/\r?\n/);
// 如果行数超过限制,截取前 PREVIEW_LINES 行并添加省略号
if (lines.length > PREVIEW_LINES) {
return lines.slice(0, PREVIEW_LINES).join("\n") + "...";
}
// 若虽然行数不超过,但总长度仍然很长,做字符级截断作为兜底
if (txt.length > PREVIEW_CHAR_LIMIT) {
return txt.slice(0, PREVIEW_CHAR_LIMIT) + "...";
}
return txt;
}); });
// 监听 text 变化:更新 textKey 并同步 isOverflow合并为单一响应函数避免冗余 // 监听 text 变化:更新 textKey 并同步 isOverflow合并为单一响应函数避免冗余
@@ -54,7 +72,8 @@ watch(
() => props.text, () => props.text,
(newText, oldText) => { (newText, oldText) => {
const textStr = newText ? String(newText) : ""; const textStr = newText ? String(newText) : "";
isOverflow.value = textStr.length > 100; const lines = textStr.split(/\r?\n/);
isOverflow.value = lines.length > PREVIEW_LINES || textStr.length > PREVIEW_CHAR_LIMIT;
if (newText !== oldText) { if (newText !== oldText) {
textKey.value++; textKey.value++;
} }

View File

@@ -111,38 +111,40 @@ export default {
`, `,
// 一级标题 // 一级标题
h1: ` h1: `
margin:4px 0; margin:8px 0;
font-size: 20px; font-size: 20px;
text-align: center; line-height: 1.6;
font-weight: bold; text-align: center;
color: ${themeColor}; font-weight: bold;
color: ${themeColor};
font-family: ${fontFamily}; font-family: ${fontFamily};
padding:3px 10px 1px; padding:6px 10px 4px;
border-bottom: 2px solid ${themeColor}; border-bottom: 2px solid ${themeColor};
border-top-right-radius:3px; border-top-right-radius:3px;
border-top-left-radius:3px; border-top-left-radius:3px;
`,
`,
// 二级标题 // 二级标题
h2: ` h2: `
margin:4px 0; margin:6px 0;
font-size: 18px; font-size: 18px;
text-align:center; line-height: 1.55;
color:${themeColor}; text-align:center;
color:${themeColor};
font-family: ${fontFamily}; font-family: ${fontFamily};
font-weight:bolder; font-weight:bolder;
padding-left:10px; padding-left:10px;
// border:1px solid ${themeColor}; // border:1px solid ${themeColor};
`, `,
// 三级标题 // 三级标题
h3: ` h3: `
margin:4px 0; margin:6px 0;
font-size: 16px; font-size: 16px;
color: ${themeColor}; line-height: 1.5;
color: ${themeColor};
font-family: ${fontFamily}; font-family: ${fontFamily};
padding-left:10px; padding-left:10px;
border-left:3px solid ${themeColor}; border-left:3px solid ${themeColor};
`, `,
// 引用 // 引用
blockquote: ` blockquote: `
margin:4px 0; margin:4px 0;
@@ -151,7 +153,7 @@ export default {
color: #777777; color: #777777;
border-left: 4px solid #dddddd; border-left: 4px solid #dddddd;
padding: 0 10px; padding: 0 10px;
`, `,
// 列表 // 列表
ul: ` ul: `
font-size: 14px; font-size: 14px;
@@ -231,25 +233,28 @@ export default {
`, `,
// 一级标题 // 一级标题
h1: ` h1: `
margin:4px 0; margin:8px 0;
font-size: 20px; font-size: 20px;
color: ${themeColor}; line-height: 1.6;
font-family: ${fontFamily}; color: ${themeColor};
`, font-family: ${fontFamily};
`,
// 二级标题 // 二级标题
h2: ` h2: `
margin:4px 0; margin:6px 0;
font-size: 18px; font-size: 18px;
color: ${themeColor}; line-height: 1.55;
font-family: ${fontFamily}; color: ${themeColor};
`, font-family: ${fontFamily};
`,
// 三级标题 // 三级标题
h3: ` h3: `
margin:4x 0; margin:6px 0;
font-size: 16px; font-size: 16px;
color: ${themeColor}; line-height: 1.5;
font-family: ${fontFamily}; color: ${themeColor};
`, font-family: ${fontFamily};
`,
// 四级标题 // 四级标题
h4: ` h4: `
margin:4px 0; margin:4px 0;