feat:更新对话框UI

This commit is contained in:
2026-07-02 23:18:02 +08:00
parent d47383cd95
commit 620424a462
7 changed files with 220 additions and 108 deletions

View File

@@ -1,79 +1,96 @@
<template>
<div class="agent-container">
<!-- 聊天区域 -->
<main class="chat-container">
<div v-for="(item, index) in agentStore.chatMessages" :key="index" class="message" :class="item.role">
<!-- 用户 -->
<div v-if="item.role === 'user'" class="bubble">
{{ item.content }}
<main class="message-container" ref="msgContainer">
<div v-if="agentStore.chatMessages.length === 0" class="empty-state">
<el-avatar :size="72" :src="aiAvatar" class="empty-avatar" />
<h2>你好我是智能家庭助手</h2>
<p>有什么可以帮你的</p>
<div class="quick-actions">
<el-button v-for="q in quickQuestions" :key="q" type="primary" round @click="askQuick(q)">{{ q }}</el-button>
</div>
<!-- AI -->
<MdPreview v-else :modelValue="item.content" class="md"/>
</div>
<div v-if="agentStore.loading" class="loading">
<el-icon class="is-loading"><Loading /></el-icon>
思考中
<div v-for="(item, index) in agentStore.chatMessages" :key="index" class="msg-row" :class="item.role">
<el-avatar :size="36" :src="item.role === 'ai' ? aiAvatar : userAvatar" class="msg-avatar"/>
<div class="msg-body">
<div class="msg-bubble" :class="item.role">
<MdPreview v-if="item.role === 'ai'" :modelValue="item.content" class="md-preview"/>
<template v-else><span class="user-text">{{ item.content }}</span></template>
</div>
<span class="msg-time">{{ item.role === 'ai' ? 'AI' : '你' }}</span>
</div>
</div>
<!-- 加载动画 -->
<div v-if="agentStore.loading" class="msg-row ai">
<el-avatar :size="36" :src="aiAvatar" class="msg-avatar" />
<div class="msg-body">
<div class="msg-bubble ai typing">
<span class="dot" />
<span class="dot" />
<span class="dot" />
</div>
</div>
</div>
</main>
<!-- 输入区 -->
<footer class="input-container">
<el-input v-model="agentStore.input" placeholder="请输入你的食材" @keyup.enter="sendMessage" clearable/>
<el-button type="primary" :loading="agentStore.loading" @click="sendMessage">发送</el-button>
<!-- 底部输入区 -->
<footer class="input-bar">
<div class="input-wrap">
<el-input v-model="agentStore.inputMessage" placeholder="输入你的问题…"
@keyup.enter="sendMessage" clearable :disabled="agentStore.loading"/>
<el-button type="primary" circle :loading="agentStore.loading"
:disabled="!agentStore.inputMessage.trim()" @click="sendMessage">
<template #icon>
<el-icon><Promotion /></el-icon>
</template>
</el-button>
</div>
</footer>
</div>
</template>
<script setup lang="ts">
import { onMounted } from "vue";
import { Loading } from "@element-plus/icons-vue";
import { MdPreview } from 'md-editor-v3';
import 'md-editor-v3/lib/preview.css';
import { Promotion } from "@element-plus/icons-vue";
import { MdPreview } from "md-editor-v3";
import "md-editor-v3/lib/preview.css";
import { useAgentStore } from "../stores/agent.ts";
import { nextTick, ref } from "vue";
import aiAvatar from '../assets/agent.svg'
import userAvatar from '../assets/user.svg'
const agentStore = useAgentStore();
const msgContainer = ref<HTMLElement>();
onMounted(() => {
getSessionMessage()
})
const quickQuestions = [
"帮我分析下这个月的账单",
"帮我查询下最近花费最大的一笔是什么?",
"帮我查询下今年存了多少钱?"
];
const getSessionMessage = async () => {
const response = await fetch(
"/chief-agent-api/chat/messages/session?username=Cxx0822",
{
method: "GET",
headers: { "Content-Type": "application/json" },
}
);
agentStore.sessionMessages = await response.json();
agentStore.threadId = crypto.randomUUID()
}
const scrollToBottom = () => {
nextTick(() => {
const el = msgContainer.value;
if (el) el.scrollTop = el.scrollHeight;
});
};
const sendMessage = async () => {
if (!agentStore.input.trim()) return;
const userMsg = agentStore.input;
agentStore.input = "";
if (!agentStore.inputMessage.trim() || agentStore.loading) return;
const userMsg = agentStore.inputMessage;
agentStore.inputMessage = "";
agentStore.chatMessages.push({ role: "user", content: userMsg });
agentStore.loading = true;
scrollToBottom();
try {
agentStore.agentMessage = {
username: 'Cxx0822',
message: userMsg,
thread_id: agentStore.threadId,
}
const response = await fetch(
"/chief-agent-api/chat/stream",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(agentStore.agentMessage)
}
);
const response = await fetch("/agent-api/query/bill", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: userMsg }),
});
agentStore.chatMessages.push({ role: "ai", content: "" });
const reader = response.body!.getReader();
@@ -83,77 +100,169 @@ const sendMessage = async () => {
while (true) {
const { value, done } = await reader.read();
if (done) break;
aiText += decoder.decode(value, { stream: true });
agentStore.chatMessages[agentStore.chatMessages.length - 1].content = aiText;
scrollToBottom();
}
} finally {
agentStore.loading = false;
scrollToBottom();
}
};
const askQuick = (q: string) => {
agentStore.inputMessage = q;
sendMessage();
};
const clearChat = () => {
agentStore.chatMessages = [];
};
</script>
<style lang="scss">
.agent-container {
height: 100%;
background-color: whitesmoke;
border-radius: 10px;
display: flex;
flex-direction: column;
gap: 10px;
padding: 5px;
overflow: hidden;
.message-container {
background: #f5f5f5;
border-radius: 12px;
.chat-container {
flex: 1;
overflow-y: auto;
padding: 10px;
.message {
margin-bottom: 12px;
}
.user {
text-align: right;
}
.ai {
text-align: left;
}
.bubble {
display: inline-block;
padding: 5px 10px;
border-radius: 14px;
max-width: 85%;
word-break: break-word;
line-height: 1.6;
background: #409eff;
color: #fff;
}
.md {
background: #f0f2f5;
border-radius: 14px;
padding: 5px;
}
}
.input-container {
background-color: white;
width: 100%;
padding: 10px;
box-sizing: border-box;
padding: 20px 16px;
display: flex;
gap: 8px;
flex-direction: column;
gap: 16px;
.empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
color: #999;
h2 {
margin: 0;
font-size: 20px;
color: #333;
}
p {
margin: 0;
font-size: 14px;
}
.quick-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
justify-content: center;
}
}
.msg-row {
display: flex;
gap: 10px;
align-items: flex-start;
max-width: 85%;
.msg-body {
display: flex;
flex-direction: column;
gap: 4px;
.msg-bubble {
padding: 10px 14px;
line-height: 1.7;
word-break: break-word;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
.md-preview {
background: transparent;
padding: 0;
font-size: 14px;
}
.user-text {
white-space: pre-wrap;
}
&.ai {
background: #fff;
color: #333;
}
&.user {
background: linear-gradient(135deg, #409eff 0%, #337ecc 100%);
color: #fff;
}
}
.msg-time {
font-size: 11px;
color: #bbb;
padding-left: 4px;
}
.typing {
display: flex;
align-items: center;
gap: 5px;
padding: 14px 18px;
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #bbb;
animation: blink 1.2s infinite both;
&:nth-child(2) { animation-delay: 0.2s; }
&:nth-child(3) { animation-delay: 0.4s; }
}
}
@keyframes blink {
0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
40% { opacity: 1; transform: scale(1); }
}
}
.msg-avatar {
flex-shrink: 0;
margin-top: 2px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
}
&.user {
flex-direction: row-reverse;
align-self: flex-end;
.msg-bubble {
border-radius: 18px 18px 4px 18px;
}
}
&.ai {
align-self: flex-start;
.msg-bubble {
border-radius: 18px 18px 18px 4px;
}
}
}
}
.loading {
text-align: center;
color: #909399;
font-size: 14px;
margin: 8px 0;
.input-bar {
background: #fff;
border-top: 1px solid #e8e8e8;
.input-wrap {
display: flex;
gap: 10px;
align-items: center;
}
}
}
</style>