Files
ylhp-ai-project-manager-fro…/src/views/ai-chat/index.vue
JiaoTianBo 5bb9b6d3db
All checks were successful
Lint Code / Lint Code (push) Successful in 3m55s
feat(ai-chat): 集成DeepChat组件重构聊天界面
- 将自定义消息列表和输入区域替换为DeepChat组件
- 新增ChatGPT组件支持历史记录传递和自定义请求处理
- 重构消息处理逻辑,简化SSE连接管理
- 改进项目选择和会话管理流程
2026-04-01 14:51:02 +08:00

612 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import {
getChatSessions,
getSessionMessages,
deleteChatSession,
buildSSEUrl,
type ChatSessionVO,
type ChatMessageVO,
type SSEStartData,
type SSEChunkData,
type SSECompleteData,
type SSEErrorData
} from "@/api/ai-chat";
import { getProjectList, type ProjectItem } from "@/api/project";
import { createSSEConnection } from "@/utils/sse/chatSSE";
import ChatGPT from "@/views/chatai/components/ChatGPT.vue";
import dayjs from "dayjs";
import AddIcon from "~icons/ri/add-line";
import DeleteIcon from "~icons/ep/delete";
import RefreshIcon from "~icons/ri/refresh-line";
defineOptions({
name: "AiChat"
});
// 状态
const loading = ref(false);
const sessions = ref<ChatSessionVO[]>([]);
const currentSession = ref<ChatSessionVO | null>(null);
const messages = ref<ChatMessageVO[]>([]);
const sending = ref(false);
const projects = ref<ProjectItem[]>([]);
const projectLoading = ref(false);
const showProjectSelect = ref(false);
const currentProjectId = ref<string>("");
const didInitProjectAndSessions = ref(false);
const activeSessionKey = ref<string>("");
const chatRenderKey = ref(0);
const deepChatHistory = ref<Array<{ text: string; role: "user" | "ai" }>>([]);
// SSE连接关闭函数
let abortSSE: (() => void) | null = null;
// 加载项目列表
async function loadProjects() {
projectLoading.value = true;
try {
const res = await getProjectList({ pageNum: 1, pageSize: 100 });
if (res.code === 200) {
projects.value = res.data?.rows || [];
}
} catch (error) {
console.error("加载项目列表失败:", error);
} finally {
projectLoading.value = false;
}
}
// 加载会话列表
async function loadSessions(projectId?: string) {
const pid =
projectId ||
currentProjectId.value ||
currentSession.value?.projectId ||
"";
if (!pid) {
sessions.value = [];
return;
}
loading.value = true;
try {
const res = await getChatSessions(pid);
if (res.code === 200) {
sessions.value = res.data || [];
}
} catch (error) {
console.error("加载会话列表失败:", error);
} finally {
loading.value = false;
}
}
function setCurrentProject(project: ProjectItem) {
const pid = String(project.id || "");
if (!pid) return;
currentProjectId.value = pid;
if (!currentSession.value) {
currentSession.value = {
sessionId: "",
sessionTitle: "新对话",
projectId: pid,
projectName: project.projectName || "",
lastMessageTime: new Date().toISOString(),
messageCount: 0,
createTime: new Date().toISOString()
};
} else if (!currentSession.value.sessionId) {
currentSession.value = {
...currentSession.value,
projectId: pid,
projectName: project.projectName || ""
};
}
}
// 选择会话
async function selectSession(session: ChatSessionVO) {
currentSession.value = session;
currentProjectId.value = String(session.projectId || "");
activeSessionKey.value = String(session.sessionId || "");
deepChatHistory.value = [];
chatRenderKey.value += 1;
await loadMessages(session.sessionId);
}
// 加载会话消息
async function loadMessages(sessionId: string) {
if (!sessionId) return;
try {
const res = await getSessionMessages(sessionId);
if (res.code === 200) {
messages.value = res.data || [];
deepChatHistory.value = (messages.value || [])
.filter(m => m.role === "user" || m.role === "assistant")
.map(m => ({
text: m.content,
role: m.role === "user" ? "user" : "ai"
}));
chatRenderKey.value += 1;
}
} catch (error) {
console.error("加载消息失败:", error);
}
}
// 创建新会话
async function createNewSession() {
if (!currentSession.value?.projectId && projects.value.length === 0) {
await loadProjects();
}
showProjectSelect.value = true;
}
// 选择项目后创建会话
async function handleProjectSelect(project: ProjectItem) {
showProjectSelect.value = false;
const pid = String(project.id || "");
if (!pid) {
ElMessage.warning("项目ID缺失无法创建会话");
return;
}
currentProjectId.value = pid;
currentSession.value = {
sessionId: "",
sessionTitle: "新对话",
projectId: pid,
projectName: project.projectName || "",
lastMessageTime: new Date().toISOString(),
messageCount: 0,
createTime: new Date().toISOString()
};
messages.value = [];
deepChatHistory.value = [];
activeSessionKey.value = `draft-${pid}-${Date.now()}`;
chatRenderKey.value += 1;
await loadSessions(currentProjectId.value);
}
async function requestAssistantReply(messageText: string): Promise<string> {
if (!messageText.trim()) return "";
if (!currentSession.value?.projectId) {
ElMessage.warning("请先选择项目");
return "请先选择项目";
}
sending.value = true;
const sseUrl = buildSSEUrl({
sessionId: currentSession.value.sessionId || undefined,
projectId: currentSession.value.projectId,
message: messageText,
useRag: true
});
if (abortSSE) {
abortSSE();
abortSSE = null;
}
let fullText = "";
const replyText = await new Promise<string>(async resolve => {
abortSSE = await createSSEConnection({
url: sseUrl,
onEvent: (eventName: string, data: any) => {
switch (eventName) {
case "start": {
const startData = data as SSEStartData;
if (startData.isNewSession || !currentSession.value?.sessionId) {
currentSession.value = {
...currentSession.value!,
sessionId: startData.sessionId,
sessionTitle: messageText.slice(0, 20)
};
loadSessions(currentSession.value.projectId);
}
break;
}
case "chunk": {
const chunkData = data as SSEChunkData;
fullText += chunkData.content;
break;
}
case "complete": {
resolve(fullText);
break;
}
case "error": {
const errorData = data as SSEErrorData;
resolve(errorData.message || "对话发生错误");
break;
}
}
},
onError: (error: Error) => {
resolve("连接服务器失败: " + error.message);
}
});
});
sending.value = false;
if (abortSSE) {
abortSSE();
abortSSE = null;
}
loadSessions(currentSession.value?.projectId);
return replyText;
}
// 删除会话
async function handleDeleteSession(session: ChatSessionVO) {
try {
await ElMessageBox.confirm("确定要删除这个会话吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
});
const res = await deleteChatSession(session.sessionId);
if (res.code === 200) {
ElMessage.success("删除成功");
loadSessions(currentSession.value?.projectId);
if (currentSession.value?.sessionId === session.sessionId) {
messages.value = [];
deepChatHistory.value = [];
const defaultProject = projects.value[0];
if (defaultProject?.id) {
currentSession.value = null;
setCurrentProject(defaultProject);
activeSessionKey.value = `draft-${String(defaultProject.id)}-${Date.now()}`;
chatRenderKey.value += 1;
} else {
currentSession.value = null;
activeSessionKey.value = "";
chatRenderKey.value += 1;
}
}
}
} catch (error) {
if (error !== "cancel") {
console.error("删除会话失败:", error);
}
}
}
// 格式化时间
function formatTime(time: string) {
return dayjs(time).format("MM-DD HH:mm");
}
// 组件卸载时关闭SSE连接
onUnmounted(() => {
if (abortSSE) {
abortSSE();
abortSSE = null;
}
});
async function initPage() {
await loadProjects();
if (!didInitProjectAndSessions.value && projects.value.length > 0) {
setCurrentProject(projects.value[0]);
didInitProjectAndSessions.value = true;
}
if (!activeSessionKey.value && currentProjectId.value) {
activeSessionKey.value = `draft-${currentProjectId.value}-${Date.now()}`;
}
await loadSessions();
const firstSession = sessions.value[0];
if (firstSession?.sessionId) {
await selectSession(firstSession);
} else {
deepChatHistory.value = [];
chatRenderKey.value += 1;
}
}
onMounted(() => {
initPage();
});
</script>
<template>
<div class="ai-chat-container">
<!-- 左侧会话列表 -->
<div class="session-sidebar">
<div class="sidebar-header">
<h3>AI 助手</h3>
<el-button
type="primary"
:icon="useRenderIcon(AddIcon)"
@click="createNewSession"
>
新对话
</el-button>
</div>
<!-- 项目选择 -->
<div v-if="showProjectSelect" class="project-select-panel">
<div class="panel-header">
<span>选择项目</span>
<el-button link @click="showProjectSelect = false">
<component :is="useRenderIcon('ri/close-line')" />
</el-button>
</div>
<el-scrollbar height="300px">
<div
v-for="project in projects"
:key="project.id"
class="project-item"
@click="handleProjectSelect(project)"
>
<component :is="useRenderIcon('ri/folder-line')" class="mr-2" />
{{ project.projectName }}
</div>
</el-scrollbar>
</div>
<!-- 会话列表 -->
<el-scrollbar class="session-list">
<div v-if="loading" class="loading-placeholder">
<el-icon class="is-loading"
><component :is="useRenderIcon('ri/loader-4-line')"
/></el-icon>
<span>加载中...</span>
</div>
<template v-else>
<div
v-for="session in sessions"
:key="session.sessionId"
:class="[
'session-item',
{ active: currentSession?.sessionId === session.sessionId }
]"
@click="selectSession(session)"
>
<div class="session-info">
<div class="session-title">{{ session.sessionTitle }}</div>
<div class="session-meta">
<span>{{ session.projectName }}</span>
<span>{{ formatTime(session.lastMessageTime) }}</span>
</div>
</div>
<el-button
link
class="delete-btn"
@click.stop="handleDeleteSession(session)"
>
<component :is="useRenderIcon(DeleteIcon)" />
</el-button>
</div>
<el-empty
v-if="sessions.length === 0"
description="暂无会话"
:image-size="80"
/>
</template>
</el-scrollbar>
</div>
<!-- 右侧聊天区域 -->
<div class="chat-main">
<template v-if="currentSession">
<!-- 会话头部 -->
<div class="chat-header">
<div class="header-info">
<h4>{{ currentSession.sessionTitle }}</h4>
<span class="project-name">{{ currentSession.projectName }}</span>
</div>
<el-button
:icon="useRenderIcon(RefreshIcon)"
:disabled="!currentSession.sessionId"
@click="loadMessages(currentSession.sessionId)"
>
刷新
</el-button>
</div>
<div class="chat-body">
<ChatGPT
:key="`${activeSessionKey}-${chatRenderKey}`"
:history="deepChatHistory"
:onRequest="requestAssistantReply"
/>
</div>
</template>
<!-- 未选择会话时的提示 -->
<div v-else class="empty-chat">
<div class="empty-icon">
<component
:is="useRenderIcon('ri/chat-3-line')"
style="font-size: 64px; color: var(--el-color-primary)"
/>
</div>
<h3>开始新的对话</h3>
<p class="text-gray-400">选择一个项目开始与AI助手对话</p>
<el-button type="primary" class="mt-4" @click="createNewSession">
<template #icon>
<component :is="useRenderIcon(AddIcon)" />
</template>
新建对话
</el-button>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.ai-chat-container {
display: flex;
height: calc(100vh - 100px);
overflow: hidden;
background: var(--el-bg-color);
border-radius: 8px;
}
.session-sidebar {
display: flex;
flex-direction: column;
width: 280px;
background: var(--el-fill-color-blank);
border-right: 1px solid var(--el-border-color-light);
.sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
border-bottom: 1px solid var(--el-border-color-light);
h3 {
margin: 0;
font-size: 16px;
}
}
.project-select-panel {
border-bottom: 1px solid var(--el-border-color-light);
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
font-weight: 500;
}
.project-item {
display: flex;
align-items: center;
padding: 12px 16px;
cursor: pointer;
transition: background 0.2s;
&:hover {
background: var(--el-fill-color-light);
}
}
}
.session-list {
flex: 1;
overflow: auto;
}
.loading-placeholder {
padding: 20px;
color: var(--el-text-color-secondary);
text-align: center;
}
.session-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
cursor: pointer;
border-bottom: 1px solid var(--el-border-color-lighter);
transition: background 0.2s;
&:hover {
background: var(--el-fill-color-light);
}
&.active {
background: var(--el-color-primary-light-9);
}
.session-info {
flex: 1;
min-width: 0;
}
.session-title {
overflow: hidden;
text-overflow: ellipsis;
font-weight: 500;
white-space: nowrap;
}
.session-meta {
display: flex;
justify-content: space-between;
margin-top: 4px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
.delete-btn {
opacity: 0;
transition: opacity 0.2s;
}
&:hover .delete-btn {
opacity: 1;
}
}
}
.chat-main {
display: flex;
flex: 1;
flex-direction: column;
min-width: 0;
.chat-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--el-border-color-light);
.header-info {
h4 {
margin: 0;
font-size: 16px;
}
.project-name {
margin-left: 8px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
}
}
.chat-body {
flex: 1;
padding: 20px;
overflow: hidden;
}
.chat-body :deep(deep-chat) {
display: block;
width: 100%;
max-width: none;
height: 100%;
}
}
.empty-chat {
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
justify-content: center;
.empty-icon {
margin-bottom: 16px;
}
h3 {
margin: 0 0 8px;
}
}
</style>