feat: 重构移动端用户聊天页面
This commit is contained in:
@@ -1,72 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import { Bot } from "@lucide/vue";
|
||||
import MarkdownIt from "markdown-it";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
messageId: string;
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
createdAt: string;
|
||||
streaming?: boolean;
|
||||
}>();
|
||||
|
||||
const markdown = new MarkdownIt({
|
||||
breaks: true,
|
||||
html: false,
|
||||
linkify: true,
|
||||
});
|
||||
const markdown = new MarkdownIt({ breaks: true, html: false, linkify: true });
|
||||
|
||||
const assistantParts = computed(() => {
|
||||
if (props.role === "user") {
|
||||
return { reasoning: "", answer: props.content };
|
||||
}
|
||||
|
||||
const content = props.content;
|
||||
let reasoning = "";
|
||||
let answer = content;
|
||||
|
||||
// 匹配所有完整的 <think>...</think>(全局匹配,支持多个)
|
||||
const thinkRegex = /<think>([\s\S]*?)<\/think>/gi;
|
||||
const matches = Array.from(answer.matchAll(thinkRegex));
|
||||
|
||||
if (matches.length > 0) {
|
||||
reasoning = matches.map((m) => m[1].trim()).join("\n\n");
|
||||
answer = answer.replace(thinkRegex, "").trim();
|
||||
}
|
||||
|
||||
// 处理未闭合的 <think>(流式输出时可能还没收到 </think>)
|
||||
const openThink = answer.match(/<think>([\s\S]*)$/i);
|
||||
if (openThink) {
|
||||
reasoning += (reasoning ? "\n\n" : "") + openThink[1].trim();
|
||||
answer = answer.slice(0, openThink.index).trim();
|
||||
}
|
||||
|
||||
// 兜底:如果 answer 为空但 reasoning 有内容,说明模型可能把全部内容放在 think 里了
|
||||
// 此时把 reasoning 当 answer 显示,不显示 thinking 面板
|
||||
if (!answer && reasoning) {
|
||||
answer = reasoning;
|
||||
reasoning = "";
|
||||
}
|
||||
|
||||
return { reasoning, answer };
|
||||
});
|
||||
|
||||
const renderedContent = computed(() => {
|
||||
const answer = computed(() => {
|
||||
if (props.role === "user") return props.content;
|
||||
return markdown.render(assistantParts.value.answer || "");
|
||||
// 部分模型会产生嵌套 think 标签,使用贪婪匹配移除完整思考区,再清理流式残留标签。
|
||||
let content = props.content.replace(/<think>[\s\S]*<\/think>/gi, "");
|
||||
content = content.replace(/<think>[\s\S]*$/i, "");
|
||||
content = content.replace(/<\/?think>/gi, "");
|
||||
return content.trim();
|
||||
});
|
||||
|
||||
const renderedContent = computed(() =>
|
||||
props.role === "assistant" ? markdown.render(answer.value) : answer.value,
|
||||
);
|
||||
|
||||
const displayTime = computed(() => {
|
||||
// 消息服务返回的是 UTC 无时区字符串,补齐时区后再按用户本地时间展示。
|
||||
const normalized = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(props.createdAt) ? props.createdAt : `${props.createdAt}Z`;
|
||||
const date = new Date(normalized);
|
||||
if (Number.isNaN(date.getTime())) return "";
|
||||
return new Intl.DateTimeFormat("zh-CN", { hour: "2-digit", minute: "2-digit", hour12: false }).format(date);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article class="message" :class="role">
|
||||
<div class="avatar">{{ role === "assistant" ? "答" : "我" }}</div>
|
||||
<div class="bubble">
|
||||
<article class="chat-message" :class="role" :data-message-id="messageId">
|
||||
<div v-if="role === 'assistant'" class="assistant-icon" aria-hidden="true">
|
||||
<Bot :size="19" />
|
||||
</div>
|
||||
<div class="message-bubble">
|
||||
<template v-if="role === 'assistant'">
|
||||
<div v-if="streaming && assistantParts.reasoning" class="thinking-indicator">
|
||||
<span class="thinking-dots">思考中</span>
|
||||
<div v-if="renderedContent" class="message-content markdown-content" v-html="renderedContent"></div>
|
||||
<div v-if="streaming && !renderedContent" class="generation-state">
|
||||
<span></span><span></span><span></span>
|
||||
正在生成回答...
|
||||
</div>
|
||||
<div class="content markdown-content" v-html="renderedContent"></div>
|
||||
</template>
|
||||
<div v-else class="content">{{ renderedContent }}</div>
|
||||
<span v-if="streaming" class="typing">正在生成</span>
|
||||
<div v-else class="message-content">{{ renderedContent }}</div>
|
||||
<time v-if="displayTime" :datetime="createdAt">{{ displayTime }}</time>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user