mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-13 20:51:09 +00:00
feat(ai): 重构AI聊天模块,使用agno框架替代langchain
- 移除langchain相关依赖,引入agno作为大模型开发框架 - 重构聊天会话和消息存储,使用agno的TeamSession管理 - 合并chat_message和chat_session模块为统一的chat模块 - 更新前端API和类型定义,适配新的后端接口 - 优化数据库日志字段类型,根据数据库类型自动选择 - 重构AI助手组件,使用新的聊天API - 清理不再使用的模型、控制器和服务代码
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
const API_PATH = "/ai/chat";
|
||||
|
||||
export const AiChatAPI = {
|
||||
getSessionList(query: {
|
||||
page_no: number;
|
||||
page_size: number;
|
||||
title?: string;
|
||||
created_at?: string[];
|
||||
updated_at?: string[];
|
||||
}) {
|
||||
return request<ApiResponse<PageResult<ChatSession[]>>>({
|
||||
url: `${API_PATH}/list`,
|
||||
method: "get",
|
||||
params: query,
|
||||
});
|
||||
},
|
||||
|
||||
createSession(body: { title: string }) {
|
||||
return request<ApiResponse<ChatSession>>({
|
||||
url: `${API_PATH}/create`,
|
||||
method: "post",
|
||||
data: body,
|
||||
});
|
||||
},
|
||||
|
||||
updateSession(id: string, body: { title: string }) {
|
||||
return request<ApiResponse<ChatSession>>({
|
||||
url: `${API_PATH}/update/${id}`,
|
||||
method: "put",
|
||||
data: body,
|
||||
});
|
||||
},
|
||||
|
||||
deleteSession(body: string[]) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/delete`,
|
||||
method: "delete",
|
||||
data: body,
|
||||
});
|
||||
},
|
||||
|
||||
chat(body: { message: string; session_id?: string | null }) {
|
||||
return request<ApiResponse<AiChatResponse>>({
|
||||
url: `${API_PATH}/ai-chat`,
|
||||
method: "post",
|
||||
data: body,
|
||||
});
|
||||
},
|
||||
|
||||
getSessionDetail(sessionId: string) {
|
||||
return request<ApiResponse<ChatSessionDetail>>({
|
||||
url: `${API_PATH}/detail/${sessionId}`,
|
||||
method: "get",
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default AiChatAPI;
|
||||
|
||||
export interface ChatSessionMessage {
|
||||
id: string;
|
||||
role: string;
|
||||
content: string;
|
||||
created_at: number | null;
|
||||
}
|
||||
|
||||
export interface ChatSession {
|
||||
session_id: string;
|
||||
agent_id: string | null;
|
||||
team_id: string | null;
|
||||
team_name: string | null;
|
||||
workflow_id: string | null;
|
||||
user_id: string | null;
|
||||
session_data: Record<string, any> | null;
|
||||
agent_data: Record<string, any> | null;
|
||||
team_data: Record<string, any> | null;
|
||||
workflow_data: Record<string, any> | null;
|
||||
metadata: Record<string, any> | null;
|
||||
runs: Array<Record<string, any>> | null;
|
||||
summary: Record<string, any> | null;
|
||||
created_at: number | null;
|
||||
updated_at: number | null;
|
||||
|
||||
id: string;
|
||||
title: string | null;
|
||||
created_time: string | null;
|
||||
updated_time: string | null;
|
||||
message_count: number;
|
||||
messages: ChatSessionMessage[];
|
||||
}
|
||||
|
||||
export interface SessionGroup {
|
||||
id: string;
|
||||
title: string;
|
||||
sessions: ChatSession[];
|
||||
}
|
||||
|
||||
export interface UserInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
username: string;
|
||||
avatar: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface AiChatResponse {
|
||||
response: string;
|
||||
session_id: string;
|
||||
function_calls: Array<{
|
||||
name: string;
|
||||
arguments: Record<string, any>;
|
||||
}> | null;
|
||||
}
|
||||
|
||||
export interface ChatSessionDetail {
|
||||
session_id: string;
|
||||
agent_id: string | null;
|
||||
team_id: string | null;
|
||||
team_name: string | null;
|
||||
workflow_id: string | null;
|
||||
user_id: string | null;
|
||||
session_data: Record<string, any> | null;
|
||||
agent_data: Record<string, any> | null;
|
||||
team_data: Record<string, any> | null;
|
||||
workflow_data: Record<string, any> | null;
|
||||
metadata: Record<string, any> | null;
|
||||
runs: Array<Record<string, any>> | null;
|
||||
summary: Record<string, any> | null;
|
||||
created_at: number | null;
|
||||
updated_at: number | null;
|
||||
|
||||
id: string;
|
||||
title: string | null;
|
||||
created_time: string | null;
|
||||
updated_time: string | null;
|
||||
message_count: number;
|
||||
messages: ChatSessionMessage[];
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
const API_PATH = "/ai/chat_message";
|
||||
|
||||
const AiChatMessageAPI = {
|
||||
listMessage(query?: MessagePageQuery) {
|
||||
return request<ApiResponse<PageResult<MessageTable[]>>>({
|
||||
url: `${API_PATH}/list`,
|
||||
method: "get",
|
||||
params: query,
|
||||
});
|
||||
},
|
||||
|
||||
detailMessage(id: number) {
|
||||
return request<ApiResponse<MessageTable>>({
|
||||
url: `${API_PATH}/detail/${id}`,
|
||||
method: "get",
|
||||
});
|
||||
},
|
||||
|
||||
getMessagesBySession(sessionId: number, query?: PageQuery) {
|
||||
return request<ApiResponse<PageResult<MessageTable[]>>>({
|
||||
url: `${API_PATH}/session/${sessionId}`,
|
||||
method: "get",
|
||||
params: query,
|
||||
});
|
||||
},
|
||||
|
||||
createMessage(body: MessageForm) {
|
||||
return request<ApiResponse<MessageTable>>({
|
||||
url: `${API_PATH}/create`,
|
||||
method: "post",
|
||||
data: body,
|
||||
});
|
||||
},
|
||||
|
||||
updateMessage(id: number, body: MessageForm) {
|
||||
return request<ApiResponse<MessageTable>>({
|
||||
url: `${API_PATH}/update/${id}`,
|
||||
method: "put",
|
||||
data: body,
|
||||
});
|
||||
},
|
||||
|
||||
deleteMessage(body: number[]) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/delete`,
|
||||
method: "delete",
|
||||
data: body,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default AiChatMessageAPI;
|
||||
|
||||
export interface MessagePageQuery extends PageQuery {
|
||||
session_id?: number;
|
||||
type?: string;
|
||||
content?: string;
|
||||
created_time?: string[];
|
||||
updated_time?: string[];
|
||||
}
|
||||
|
||||
export interface MessageTable extends BaseType {
|
||||
session_id: number;
|
||||
type: "user" | "assistant";
|
||||
content: string;
|
||||
timestamp: number;
|
||||
files?: Record<string, any>[];
|
||||
}
|
||||
|
||||
export interface MessageForm extends BaseFormType {
|
||||
session_id?: number;
|
||||
type?: "user" | "assistant";
|
||||
content?: string;
|
||||
timestamp?: number;
|
||||
files?: Record<string, any>[];
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
id: string;
|
||||
type: "user" | "assistant";
|
||||
content: string;
|
||||
timestamp: number;
|
||||
loading?: boolean;
|
||||
collapsed?: boolean;
|
||||
files?: UploadedFile[];
|
||||
}
|
||||
|
||||
export interface UploadedFile {
|
||||
id: string;
|
||||
name: string;
|
||||
size: number;
|
||||
type: string;
|
||||
url?: string;
|
||||
file?: File;
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
const API_PATH = "/ai/chat_session";
|
||||
|
||||
const AiChatSessionAPI = {
|
||||
listSession(query?: SessionPageQuery) {
|
||||
return request<ApiResponse<PageResult<SessionTable[]>>>({
|
||||
url: `${API_PATH}/list`,
|
||||
method: "get",
|
||||
params: query,
|
||||
});
|
||||
},
|
||||
|
||||
detailSession(id: number) {
|
||||
return request<ApiResponse<SessionTable>>({
|
||||
url: `${API_PATH}/detail/${id}`,
|
||||
method: "get",
|
||||
});
|
||||
},
|
||||
|
||||
createSession(body: SessionForm) {
|
||||
return request<ApiResponse<SessionTable>>({
|
||||
url: `${API_PATH}/create`,
|
||||
method: "post",
|
||||
data: body,
|
||||
});
|
||||
},
|
||||
|
||||
updateSession(id: number, body: SessionForm) {
|
||||
return request<ApiResponse<SessionTable>>({
|
||||
url: `${API_PATH}/update/${id}`,
|
||||
method: "put",
|
||||
data: body,
|
||||
});
|
||||
},
|
||||
|
||||
deleteSession(body: number[]) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/delete`,
|
||||
method: "delete",
|
||||
data: body,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default AiChatSessionAPI;
|
||||
|
||||
export interface SessionPageQuery extends PageQuery {
|
||||
title?: string;
|
||||
status?: string;
|
||||
created_time?: string[];
|
||||
updated_time?: string[];
|
||||
}
|
||||
|
||||
export interface SessionTable extends BaseType {
|
||||
title: string;
|
||||
created_by?: CommonType;
|
||||
updated_by?: CommonType;
|
||||
}
|
||||
|
||||
export interface SessionForm extends BaseFormType {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface ChatSession {
|
||||
id: number;
|
||||
title: string;
|
||||
created_time: string;
|
||||
updated_time: string;
|
||||
message_count?: number;
|
||||
created_by?: CommonType;
|
||||
updated_by?: CommonType;
|
||||
}
|
||||
|
||||
export interface SessionGroup {
|
||||
title: string;
|
||||
sessions: ChatSession[];
|
||||
}
|
||||
|
||||
export interface UserInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
username: string;
|
||||
avatar?: string;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
const API_PATH = "/application/ai";
|
||||
|
||||
export const McpAPI = {
|
||||
/**
|
||||
* 查询应用列表
|
||||
* @param query 查询参数
|
||||
*/
|
||||
chatMcp(query: AiChatQuery) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/chat`,
|
||||
method: "post",
|
||||
params: query,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default McpAPI;
|
||||
|
||||
/**
|
||||
* 应用表单
|
||||
*/
|
||||
export interface AiChatQuery {
|
||||
message: string;
|
||||
}
|
||||
@@ -119,11 +119,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onBeforeUnmount, onMounted, watch } from "vue";
|
||||
import { nextTick, onBeforeUnmount, onMounted, watch, ref, computed } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { useSettingsStore } from "@/store";
|
||||
import McpAPI from "@/api/module_application/mcp";
|
||||
import { AiChatAPI, ChatSession, ChatSessionDetail } from "@/api/module_ai/chat";
|
||||
|
||||
type ToolFunctionCall = {
|
||||
name: string;
|
||||
@@ -165,6 +165,12 @@ const command = ref("");
|
||||
const loading = ref(false);
|
||||
const response = ref<AiResponse | null>(null);
|
||||
|
||||
// 会话管理
|
||||
const sessions = ref<ChatSession[]>([]);
|
||||
const currentSessionId = ref<string | null>(null);
|
||||
const currentSession = ref<ChatSessionDetail | null>(null);
|
||||
const sessionLoading = ref(false);
|
||||
|
||||
const fabCollapsed = useStorage<boolean>("vea:ui:ai_assistant_fab_collapsed", false);
|
||||
|
||||
const fabRight = ref(30);
|
||||
@@ -300,11 +306,46 @@ const examples = [
|
||||
"获取姓名为张三的用户信息",
|
||||
];
|
||||
|
||||
// 加载会话列表
|
||||
const loadSessions = async () => {
|
||||
try {
|
||||
sessionLoading.value = true;
|
||||
const response = await AiChatAPI.getSessionList({ page_no: 1, page_size: 100 });
|
||||
if (response.data?.success) {
|
||||
sessions.value = response.data.data?.items || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载会话列表失败:", error);
|
||||
} finally {
|
||||
sessionLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 加载会话详情
|
||||
const loadSessionDetail = async (sessionId: string) => {
|
||||
try {
|
||||
sessionLoading.value = true;
|
||||
const response = await AiChatAPI.getSessionDetail(sessionId);
|
||||
if (response.data?.success) {
|
||||
currentSessionId.value = sessionId;
|
||||
currentSession.value = response.data.data;
|
||||
} else {
|
||||
ElMessage.error("获取会话详情失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取会话详情失败:", error);
|
||||
ElMessage.error("获取会话详情失败");
|
||||
} finally {
|
||||
sessionLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 打开对话框
|
||||
const handleOpen = () => {
|
||||
dialogVisible.value = true;
|
||||
command.value = "";
|
||||
response.value = null;
|
||||
loadSessions();
|
||||
};
|
||||
|
||||
// 关闭对话框
|
||||
@@ -334,8 +375,9 @@ const handleExecute = async () => {
|
||||
|
||||
try {
|
||||
// 调用 AI API 解析命令
|
||||
const result = await McpAPI.chatMcp({
|
||||
const result = await AiChatAPI.chat({
|
||||
message: rawCommand,
|
||||
session_id: currentSessionId.value || undefined,
|
||||
});
|
||||
|
||||
if (!result.data.success) {
|
||||
@@ -343,10 +385,18 @@ const handleExecute = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新会话ID
|
||||
if (result.data.data?.session_id) {
|
||||
currentSessionId.value = result.data.data.session_id;
|
||||
// 重新加载会话列表和详情
|
||||
await loadSessions();
|
||||
await loadSessionDetail(result.data.data.session_id);
|
||||
}
|
||||
|
||||
// 解析 AI 返回的操作类型
|
||||
const action = parseAction(result.data, rawCommand);
|
||||
const action = parseAction(result.data.data, rawCommand);
|
||||
response.value = {
|
||||
explanation: result.data.msg ?? "命令解析成功,准备执行操作",
|
||||
explanation: result.data.data?.response ?? "命令解析成功,准备执行操作",
|
||||
action,
|
||||
};
|
||||
|
||||
@@ -475,7 +525,9 @@ const tryDirectNavigate = (rawCommand: string): AiResponse | null => {
|
||||
// 解析 AI 返回的操作类型
|
||||
const parseAction = (result: any, rawCommand: string): AiAction | null => {
|
||||
const cmd = normalizeText(rawCommand);
|
||||
const primaryCall = result.functionCalls?.[0];
|
||||
// 后端返回的是 function_calls(下划线命名)
|
||||
const functionCalls = result.function_calls || result.functionCalls;
|
||||
const primaryCall = functionCalls?.[0];
|
||||
const functionName = primaryCall?.name;
|
||||
|
||||
// 优先从函数名推断路由,其次从命令文本匹配
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
import { ref, computed } from "vue";
|
||||
import { Promotion, Paperclip, Document, Close } from "@element-plus/icons-vue";
|
||||
import type { UploadFile } from "element-plus";
|
||||
import type { UploadedFile } from "@/api/module_ai/chat_message";
|
||||
import type { UploadedFile } from "@/views/module_ai/chat/types";
|
||||
|
||||
interface Props {
|
||||
disabled?: boolean;
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import { ref, nextTick, watch } from "vue";
|
||||
import WelcomeScreen from "./WelcomeScreen.vue";
|
||||
import MessageItem from "./MessageItem.vue";
|
||||
import type { ChatMessage } from "@/api/module_ai/chat_message";
|
||||
import type { ChatMessage } from "@/views/module_ai/chat/types";
|
||||
|
||||
interface Props {
|
||||
messages: ChatMessage[];
|
||||
|
||||
@@ -77,7 +77,7 @@ import hljs from "highlight.js";
|
||||
import DOMPurify from "dompurify";
|
||||
import "highlight.js/styles/atom-one-light.css";
|
||||
import { useUserStoreHook } from "@/store";
|
||||
import type { ChatMessage } from "@/api/module_ai/chat_message";
|
||||
import type { ChatMessage } from "@/views/module_ai/chat/types";
|
||||
|
||||
interface Props {
|
||||
message: ChatMessage;
|
||||
|
||||
@@ -46,7 +46,9 @@
|
||||
@click="handleSelectSession(session)"
|
||||
>
|
||||
<el-icon class="session-icon"><ChatLineRound /></el-icon>
|
||||
<span class="session-title">{{ session.title }}</span>
|
||||
<span class="session-title">
|
||||
{{ session.title || session.session_data?.session_name || "未命名会话" }}
|
||||
</span>
|
||||
<el-dropdown
|
||||
trigger="click"
|
||||
@command="(cmd) => handleSessionCommand(cmd, session)"
|
||||
@@ -122,10 +124,11 @@ import {
|
||||
ArrowDown,
|
||||
} from "@element-plus/icons-vue";
|
||||
import { useUserStoreHook } from "@/store";
|
||||
import AiAPI, { ChatSession, SessionGroup, UserInfo } from "@/api/module_ai/chat_session";
|
||||
import { ChatSession, SessionGroup, UserInfo } from "@/api/module_ai/chat";
|
||||
import AiChatAPI from "@/api/module_ai/chat";
|
||||
|
||||
interface Props {
|
||||
currentSessionId?: number | null;
|
||||
currentSessionId?: string | null;
|
||||
isCollapsed?: boolean;
|
||||
}
|
||||
|
||||
@@ -157,7 +160,9 @@ const filteredSessions = computed<ChatSession[]>(() => {
|
||||
return sessions.value;
|
||||
}
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return sessions.value.filter((session) => session.title.toLowerCase().includes(query));
|
||||
return sessions.value.filter((session: ChatSession) =>
|
||||
(session.title || "").toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
|
||||
const groupedSessions = computed<SessionGroup[]>(() => {
|
||||
@@ -173,7 +178,9 @@ const groupedSessions = computed<SessionGroup[]>(() => {
|
||||
const earlierSessions: ChatSession[] = [];
|
||||
|
||||
filteredSessions.value.forEach((session) => {
|
||||
const updatedTime = new Date(session.updated_time).getTime();
|
||||
if (!session.updated_at) return;
|
||||
// Unix 时间戳(秒)转换为毫秒
|
||||
const updatedTime = session.updated_at * 1000;
|
||||
|
||||
if (updatedTime >= todayStart) {
|
||||
todaySessions.push(session);
|
||||
@@ -189,19 +196,19 @@ const groupedSessions = computed<SessionGroup[]>(() => {
|
||||
const groups: SessionGroup[] = [];
|
||||
|
||||
if (todaySessions.length > 0) {
|
||||
groups.push({ title: "今天", sessions: todaySessions });
|
||||
groups.push({ id: "today", title: "今天", sessions: todaySessions });
|
||||
}
|
||||
|
||||
if (yesterdaySessions.length > 0) {
|
||||
groups.push({ title: "昨天", sessions: yesterdaySessions });
|
||||
groups.push({ id: "yesterday", title: "昨天", sessions: yesterdaySessions });
|
||||
}
|
||||
|
||||
if (weekSessions.length > 0) {
|
||||
groups.push({ title: "本周", sessions: weekSessions });
|
||||
groups.push({ id: "week", title: "本周", sessions: weekSessions });
|
||||
}
|
||||
|
||||
if (earlierSessions.length > 0) {
|
||||
groups.push({ title: "更早", sessions: earlierSessions });
|
||||
groups.push({ id: "earlier", title: "更早", sessions: earlierSessions });
|
||||
}
|
||||
|
||||
return groups;
|
||||
@@ -236,7 +243,7 @@ const handleSessionCommand = async (command: string, session: ChatSession) => {
|
||||
inputPattern: /.+/,
|
||||
inputErrorMessage: "会话名称不能为空",
|
||||
});
|
||||
await AiAPI.updateSession(session.id, { title: value });
|
||||
await AiChatAPI.updateSession(session.id, { title: value });
|
||||
session.title = value;
|
||||
ElMessage.success("重命名成功");
|
||||
} catch (error) {
|
||||
@@ -253,8 +260,8 @@ const handleSessionCommand = async (command: string, session: ChatSession) => {
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
});
|
||||
await AiAPI.deleteSession([session.id]);
|
||||
const index = sessions.value.findIndex((s) => s.id === session.id);
|
||||
await AiChatAPI.deleteSession([session.id]);
|
||||
const index = sessions.value.findIndex((s: ChatSession) => s.id === session.id);
|
||||
if (index > -1) {
|
||||
sessions.value.splice(index, 1);
|
||||
}
|
||||
@@ -282,16 +289,36 @@ const handleUserCommand = (command: string) => {
|
||||
|
||||
const loadSessions = async () => {
|
||||
try {
|
||||
const res = await AiAPI.listSession({ page_no: 1, page_size: 100 });
|
||||
if (res.data?.code === 0 || res.data?.success === true) {
|
||||
sessions.value = (res.data.data?.items || [])
|
||||
.filter((item: any) => item.id !== undefined)
|
||||
const res = await AiChatAPI.getSessionList({ page_no: 1, page_size: 100 });
|
||||
const responseData = res.data;
|
||||
const data = responseData?.data;
|
||||
|
||||
if (data?.items && Array.isArray(data.items)) {
|
||||
sessions.value = data.items
|
||||
.filter((item: any) => item.session_id !== undefined)
|
||||
.map((item: any) => ({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
created_time: item.created_time,
|
||||
updated_time: item.updated_time,
|
||||
message_count: 0,
|
||||
id: item.session_id,
|
||||
title: item.session_data?.session_name || item.session_id?.slice(0, 8) || "新会话",
|
||||
created_at: item.created_at,
|
||||
updated_at: item.updated_at,
|
||||
message_count: item.runs?.length || 0,
|
||||
session_id: item.session_id,
|
||||
session_type: item.session_type,
|
||||
agent_id: item.agent_id,
|
||||
user_id: item.user_id,
|
||||
team_id: item.team_id,
|
||||
team_name: item.team_name,
|
||||
workflow_id: item.workflow_id,
|
||||
summary: item.summary,
|
||||
metadata: item.metadata,
|
||||
runs: item.runs,
|
||||
session_data: item.session_data,
|
||||
agent_data: item.agent_data,
|
||||
team_data: item.team_data,
|
||||
workflow_data: item.workflow_data,
|
||||
created_time: item.created_at ? new Date(item.created_at * 1000).toISOString() : null,
|
||||
updated_time: item.updated_at ? new Date(item.updated_at * 1000).toISOString() : null,
|
||||
messages: item.runs?.flatMap((run: any) => run.messages || []) || [],
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
ref="chatMessagesRef"
|
||||
:messages="messages"
|
||||
:error="error"
|
||||
@prompt-click="handlePromptClick"
|
||||
@prompt-click="handleSendMessage"
|
||||
@error-close="error = ''"
|
||||
/>
|
||||
</el-main>
|
||||
@@ -49,75 +49,69 @@ defineOptions({
|
||||
name: "Chat",
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import ChatNavbar from "./components/ChatNavbar.vue";
|
||||
import ChatMessages from "./components/ChatMessages.vue";
|
||||
import ChatInput from "./components/ChatInput.vue";
|
||||
import Sidebar from "./components/Sidebar.vue";
|
||||
import AiChatSessionAPI, { ChatSession } from "@/api/module_ai/chat_session";
|
||||
import AiChatMessageAPI, { ChatMessage, UploadedFile } from "@/api/module_ai/chat_message";
|
||||
import AiChatAPI, { ChatSession } from "@/api/module_ai/chat";
|
||||
import { Auth } from "@/utils/auth";
|
||||
import type { ChatMessage, UploadedFile } from "./types";
|
||||
|
||||
// 状态
|
||||
const messages = ref<ChatMessage[]>([]);
|
||||
const sending = ref(false);
|
||||
const isConnected = ref(false);
|
||||
const connectionStatus = ref<"connected" | "connecting" | "disconnected">("disconnected");
|
||||
const error = ref<string>("");
|
||||
const chatMessagesRef = ref<{ scrollToBottom: () => void }>();
|
||||
const sidebarRef = ref<{ loadSessions: () => void }>();
|
||||
const currentSessionId = ref<number | null>(null);
|
||||
const error = ref("");
|
||||
const currentSessionId = ref<string | null>(null);
|
||||
const isSidebarCollapsed = ref(false);
|
||||
|
||||
// Refs
|
||||
const chatMessagesRef = ref<{ scrollToBottom: () => void }>();
|
||||
const sidebarRef = ref<{ loadSessions: () => void }>();
|
||||
|
||||
// WebSocket
|
||||
let ws: WebSocket | null = null;
|
||||
const WS_URL = import.meta.env.VITE_APP_WS_ENDPOINT;
|
||||
|
||||
// ============ WebSocket 操作 ============
|
||||
const connectWebSocket = () => {
|
||||
if (ws?.readyState === WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
if (ws?.readyState === WebSocket.OPEN) return;
|
||||
|
||||
connectionStatus.value = "connecting";
|
||||
error.value = "";
|
||||
|
||||
try {
|
||||
// WebSocket连接不支持直接设置请求头
|
||||
// 这里使用URL参数传递token,后端需要相应处理
|
||||
const url = new URL("/api/v1/ai/chat/ws", WS_URL);
|
||||
const token = Auth.getAccessToken();
|
||||
if (token) {
|
||||
url.searchParams.append("token", token);
|
||||
}
|
||||
if (token) url.searchParams.append("token", token);
|
||||
|
||||
ws = new WebSocket(url.toString());
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log("WebSocket 连接已建立");
|
||||
isConnected.value = true;
|
||||
connectionStatus.value = "connected";
|
||||
ElMessage.success("连接成功");
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
handleWebSocketMessage({ content: event.data });
|
||||
};
|
||||
ws.onmessage = (event) => handleWebSocketMessage(event.data);
|
||||
|
||||
ws.onclose = (event) => {
|
||||
console.log("WebSocket 连接已关闭", event.code, event.reason);
|
||||
ws.onclose = () => {
|
||||
isConnected.value = false;
|
||||
connectionStatus.value = "disconnected";
|
||||
finishLoadingMessages();
|
||||
};
|
||||
|
||||
ws.onerror = (err) => {
|
||||
console.error("WebSocket 错误:", err);
|
||||
ws.onerror = () => {
|
||||
isConnected.value = false;
|
||||
connectionStatus.value = "disconnected";
|
||||
ElMessage.error("连接失败,请检查服务器状态");
|
||||
finishLoadingMessages();
|
||||
};
|
||||
} catch (err) {
|
||||
console.error("创建 WebSocket 连接失败:", err);
|
||||
} catch {
|
||||
connectionStatus.value = "disconnected";
|
||||
error.value = "无法创建连接";
|
||||
}
|
||||
@@ -142,78 +136,85 @@ const toggleConnection = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleWebSocketMessage = (data: any) => {
|
||||
// ============ 消息处理 ============
|
||||
const handleWebSocketMessage = (data: string) => {
|
||||
const lastMessage = messages.value[messages.value.length - 1];
|
||||
const content = data || "";
|
||||
|
||||
if (lastMessage && lastMessage.type === "assistant" && lastMessage.loading) {
|
||||
lastMessage.content += data.content || data.message || "";
|
||||
if (lastMessage?.type === "assistant" && lastMessage.loading) {
|
||||
lastMessage.content += content;
|
||||
} else {
|
||||
addMessage("assistant", data.content || data.message || "收到回复");
|
||||
addMessage("assistant", content);
|
||||
}
|
||||
|
||||
chatMessagesRef.value?.scrollToBottom();
|
||||
};
|
||||
|
||||
const handleSendMessage = async (message: string, files?: UploadedFile[]) => {
|
||||
if ((!message && !files) || !isConnected.value || sending.value) {
|
||||
return;
|
||||
}
|
||||
const addMessage = (type: "user" | "assistant", content: string, files?: UploadedFile[]) => {
|
||||
messages.value.push({
|
||||
id: generateId(),
|
||||
type,
|
||||
content,
|
||||
timestamp: Date.now(),
|
||||
collapsed: content.length > 200,
|
||||
files,
|
||||
});
|
||||
};
|
||||
|
||||
const lastMessage = messages.value[messages.value.length - 1];
|
||||
if (lastMessage && lastMessage.type === "assistant" && lastMessage.loading) {
|
||||
lastMessage.loading = false;
|
||||
}
|
||||
|
||||
// 如果没有当前会话,先创建会话
|
||||
if (!currentSessionId.value) {
|
||||
try {
|
||||
// 使用第一条消息的前20个字符作为会话标题
|
||||
const title = message.substring(0, 20) + (message.length > 20 ? "..." : "");
|
||||
const res = await AiChatSessionAPI.createSession({ title });
|
||||
if (res.data?.code === 0 || res.data?.success === true) {
|
||||
currentSessionId.value = res.data.data.id ?? null;
|
||||
sidebarRef.value?.loadSessions();
|
||||
} else {
|
||||
throw new Error("创建会话失败");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("创建会话失败:", err);
|
||||
ElMessage.error("创建会话失败,请重试");
|
||||
return;
|
||||
const finishLoadingMessages = () => {
|
||||
messages.value.forEach((msg) => {
|
||||
if (msg.type === "assistant" && msg.loading) {
|
||||
msg.loading = false;
|
||||
msg.collapsed = msg.content.length > 200;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const generateId = () => {
|
||||
return Date.now().toString(36) + Math.random().toString(36).slice(2);
|
||||
};
|
||||
|
||||
// ============ 发送消息 ============
|
||||
const handleSendMessage = async (message: string, files?: UploadedFile[]) => {
|
||||
if ((!message && !files) || !isConnected.value || sending.value) return;
|
||||
|
||||
// 结束上一个加载中的消息
|
||||
finishLoadingMessages();
|
||||
|
||||
// 创建新会话(如果没有)
|
||||
if (!currentSessionId.value) {
|
||||
const success = await createNewSession(message);
|
||||
if (!success) return;
|
||||
}
|
||||
|
||||
// 添加用户消息
|
||||
addMessage("user", message, files);
|
||||
|
||||
const loadingMessage: ChatMessage = {
|
||||
// 添加加载中的助手消息
|
||||
messages.value.push({
|
||||
id: generateId(),
|
||||
type: "assistant",
|
||||
content: "",
|
||||
timestamp: Date.now(),
|
||||
loading: true,
|
||||
};
|
||||
messages.value.push(loadingMessage);
|
||||
});
|
||||
|
||||
sending.value = true;
|
||||
chatMessagesRef.value?.scrollToBottom();
|
||||
|
||||
try {
|
||||
if (ws?.readyState === WebSocket.OPEN) {
|
||||
const payload = {
|
||||
message,
|
||||
session_id: currentSessionId.value,
|
||||
files: files?.map((f) => ({
|
||||
name: f.name,
|
||||
type: f.type,
|
||||
size: f.size,
|
||||
})),
|
||||
};
|
||||
ws.send(JSON.stringify(payload));
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
message,
|
||||
session_id: currentSessionId.value,
|
||||
files: files?.map((f) => ({ name: f.name, type: f.type, size: f.size })),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
throw new Error("WebSocket 连接未建立");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("发送消息失败:", err);
|
||||
} catch {
|
||||
messages.value.pop();
|
||||
error.value = "发送消息失败,请检查连接状态";
|
||||
ElMessage.error("发送失败");
|
||||
@@ -222,16 +223,57 @@ const handleSendMessage = async (message: string, files?: UploadedFile[]) => {
|
||||
}
|
||||
};
|
||||
|
||||
const addMessage = (type: "user" | "assistant", content: string, files?: UploadedFile[]) => {
|
||||
const message: ChatMessage = {
|
||||
id: generateId(),
|
||||
type,
|
||||
content,
|
||||
timestamp: Date.now(),
|
||||
collapsed: content.length > 200,
|
||||
files,
|
||||
};
|
||||
messages.value.push(message);
|
||||
const createNewSession = async (firstMessage: string): Promise<boolean> => {
|
||||
try {
|
||||
const title = firstMessage.slice(0, 20) + (firstMessage.length > 20 ? "..." : "");
|
||||
const res = await AiChatAPI.createSession({ title });
|
||||
|
||||
if (res.data?.code === 0 || res.data?.success) {
|
||||
currentSessionId.value = res.data.data?.id ?? null;
|
||||
sidebarRef.value?.loadSessions();
|
||||
return true;
|
||||
}
|
||||
throw new Error("创建会话失败");
|
||||
} catch {
|
||||
ElMessage.error("创建会话失败,请重试");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// ============ 会话操作 ============
|
||||
const handleSelectSession = async (session: ChatSession) => {
|
||||
currentSessionId.value = session.id;
|
||||
messages.value = [];
|
||||
|
||||
try {
|
||||
const response = await AiChatAPI.getSessionDetail(session.id);
|
||||
if (response.data?.code !== 0) {
|
||||
ElMessage.error("获取会话详情失败");
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionData = response.data.data || {};
|
||||
const runs = sessionData.runs || [];
|
||||
|
||||
runs.forEach((run: any) => {
|
||||
const runMessages = run.messages || [];
|
||||
runMessages.forEach((msg: any) => {
|
||||
if (msg.role === "user" || msg.role === "assistant") {
|
||||
addMessage(msg.role, msg.content);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ElMessage.success(`已切换到会话:${session.title}`);
|
||||
} catch {
|
||||
ElMessage.error("获取会话详情失败");
|
||||
}
|
||||
};
|
||||
|
||||
const handleNewSession = () => {
|
||||
currentSessionId.value = null;
|
||||
messages.value = [];
|
||||
ElMessage.success("已开启新对话");
|
||||
};
|
||||
|
||||
const handleClearChat = async () => {
|
||||
@@ -248,74 +290,13 @@ const handleClearChat = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handlePromptClick = (prompt: string) => {
|
||||
handleSendMessage(prompt);
|
||||
};
|
||||
|
||||
const finishLoadingMessages = () => {
|
||||
messages.value.forEach((message) => {
|
||||
if (message.type === "assistant" && message.loading) {
|
||||
message.loading = false;
|
||||
message.collapsed = message.content.length > 200;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const generateId = () => {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||
};
|
||||
|
||||
const handleSelectSession = async (session: ChatSession) => {
|
||||
currentSessionId.value = session.id;
|
||||
messages.value = [];
|
||||
await loadMessages(session.id);
|
||||
ElMessage.success(`已切换到会话:${session.title}`);
|
||||
};
|
||||
|
||||
const handleNewSession = async () => {
|
||||
currentSessionId.value = null;
|
||||
messages.value = [];
|
||||
ElMessage.success("已开启新对话");
|
||||
};
|
||||
|
||||
const loadMessages = async (sessionId: number) => {
|
||||
try {
|
||||
const res = await AiChatMessageAPI.getMessagesBySession(sessionId, {
|
||||
page_no: 1,
|
||||
page_size: 100,
|
||||
});
|
||||
if (res.data?.code === 0 || res.data?.success === true) {
|
||||
const apiMessages = res.data.data?.items || [];
|
||||
messages.value = apiMessages.map((msg: any) => ({
|
||||
id: msg.id.toString(),
|
||||
type: msg.type as "user" | "assistant",
|
||||
content: msg.content,
|
||||
timestamp: msg.timestamp,
|
||||
collapsed: msg.content.length > 200,
|
||||
files: msg.files?.map((f: any) => ({
|
||||
id: generateId(),
|
||||
name: f.name || "",
|
||||
size: f.size || 0,
|
||||
type: f.type || "",
|
||||
})),
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载消息失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSidebar = () => {
|
||||
isSidebarCollapsed.value = !isSidebarCollapsed.value;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
connectWebSocket();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
disconnectWebSocket();
|
||||
});
|
||||
// ============ 生命周期 ============
|
||||
onMounted(connectWebSocket);
|
||||
onUnmounted(disconnectWebSocket);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// 聊天消息类型
|
||||
export interface ChatMessage {
|
||||
id: string;
|
||||
type: "user" | "assistant";
|
||||
content: string;
|
||||
timestamp: number;
|
||||
loading?: boolean;
|
||||
collapsed?: boolean;
|
||||
files?: UploadedFile[];
|
||||
}
|
||||
|
||||
// 上传文件类型
|
||||
export interface UploadedFile {
|
||||
id: string;
|
||||
name: string;
|
||||
size: number;
|
||||
type: string;
|
||||
url?: string;
|
||||
file?: File;
|
||||
}
|
||||
|
||||
// 会话消息类型(来自后端)
|
||||
export interface SessionMessage {
|
||||
id: string;
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
created_time?: string;
|
||||
}
|
||||
|
||||
// 会话详情类型
|
||||
export interface SessionDetail {
|
||||
id: string;
|
||||
title: string;
|
||||
created_time?: string;
|
||||
updated_time?: string;
|
||||
message_count: number;
|
||||
messages: SessionMessage[];
|
||||
}
|
||||
@@ -1,604 +0,0 @@
|
||||
<!-- AI智能助手消息列表 -->
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 内容区域 -->
|
||||
<el-card class="data-table">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>
|
||||
对话消息列表
|
||||
<el-tooltip content="对话消息列表">
|
||||
<QuestionFilled class="w-4 h-4 mx-1" />
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
<!-- 搜索区域 -->
|
||||
<div v-show="visible" class="search-container">
|
||||
<el-form
|
||||
ref="queryFormRef"
|
||||
:model="queryFormData"
|
||||
label-suffix=":"
|
||||
:inline="true"
|
||||
@submit.prevent="handleQuery"
|
||||
>
|
||||
<el-form-item prop="session_id" label="会话ID">
|
||||
<el-input v-model="queryFormData.session_id" placeholder="请输入会话ID" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item prop="type" label="类型">
|
||||
<el-select
|
||||
v-model="queryFormData.type"
|
||||
placeholder="请选择类型"
|
||||
style="width: 170px"
|
||||
clearable
|
||||
>
|
||||
<el-option value="user" label="用户" />
|
||||
<el-option value="assistant" label="助手" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item prop="content" label="内容">
|
||||
<el-input v-model="queryFormData.content" placeholder="请输入内容" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="isExpand" prop="created_time" label="创建时间">
|
||||
<DatePicker
|
||||
v-model="createdDateRange"
|
||||
@update:model-value="handleCreatedDateRangeChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- 查询、重置、展开/收起按钮 -->
|
||||
<el-form-item>
|
||||
<el-button
|
||||
v-hasPerm="['module_example:demo:query']"
|
||||
type="primary"
|
||||
icon="search"
|
||||
@click="handleQuery"
|
||||
>
|
||||
查询
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPerm="['module_example:demo:query']"
|
||||
icon="refresh"
|
||||
@click="handleResetQuery"
|
||||
>
|
||||
重置
|
||||
</el-button>
|
||||
<!-- 展开/收起 -->
|
||||
<template v-if="isExpandable">
|
||||
<el-link
|
||||
class="ml-3"
|
||||
type="primary"
|
||||
underline="never"
|
||||
@click="isExpand = !isExpand"
|
||||
>
|
||||
{{ isExpand ? "收起" : "展开" }}
|
||||
<el-icon>
|
||||
<template v-if="isExpand">
|
||||
<ArrowUp />
|
||||
</template>
|
||||
<template v-else>
|
||||
<ArrowDown />
|
||||
</template>
|
||||
</el-icon>
|
||||
</el-link>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 功能区域 -->
|
||||
<div class="data-table__toolbar">
|
||||
<div class="data-table__toolbar--left">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
v-hasPerm="['module_ai:chat_message:create']"
|
||||
type="success"
|
||||
icon="plus"
|
||||
@click="handleOpenDialog('create')"
|
||||
>
|
||||
新增
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
v-hasPerm="['module_ai:chat_message:delete']"
|
||||
type="danger"
|
||||
icon="delete"
|
||||
:disabled="selectIds.length === 0"
|
||||
@click="handleDelete(selectIds)"
|
||||
>
|
||||
批量删除
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<div class="data-table__toolbar--right">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="搜索显示/隐藏">
|
||||
<el-button
|
||||
v-hasPerm="['*:*:*']"
|
||||
type="info"
|
||||
icon="search"
|
||||
circle
|
||||
@click="visible = !visible"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="刷新">
|
||||
<el-button
|
||||
v-hasPerm="['module_ai:chat_message:query']"
|
||||
type="primary"
|
||||
icon="refresh"
|
||||
circle
|
||||
@click="handleRefresh"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格区域:系统配置列表 -->
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
v-loading="loading"
|
||||
:data="pageTableData"
|
||||
highlight-current-row
|
||||
class="data-table__content"
|
||||
height="calc(100vh - 440px)"
|
||||
max-height="calc(100vh - 440px)"
|
||||
border
|
||||
stripe
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<template #empty>
|
||||
<el-empty :image-size="80" description="暂无数据" />
|
||||
</template>
|
||||
<el-table-column
|
||||
v-if="tableColumns.find((col) => col.prop === 'selection')?.show"
|
||||
type="selection"
|
||||
min-width="55"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="tableColumns.find((col) => col.prop === 'index')?.show"
|
||||
fixed
|
||||
label="序号"
|
||||
min-width="60"
|
||||
>
|
||||
<template #default="scope">
|
||||
{{ (queryFormData.page_no - 1) * queryFormData.page_size + scope.$index + 1 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="tableColumns.find((col) => col.prop === 'session_id')?.show"
|
||||
label="会话ID"
|
||||
prop="session_id"
|
||||
min-width="120"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="tableColumns.find((col) => col.prop === 'type')?.show"
|
||||
label="类型"
|
||||
prop="type"
|
||||
min-width="100"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.type === 'user' ? 'primary' : 'success'">
|
||||
{{ scope.row.type === "user" ? "用户" : "助手" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="tableColumns.find((col) => col.prop === 'content')?.show"
|
||||
label="内容"
|
||||
prop="content"
|
||||
min-width="200"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="scope">
|
||||
<MarkdownRenderer :content="scope.row.content" :max-length="100" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="tableColumns.find((col) => col.prop === 'timestamp')?.show"
|
||||
label="时间戳"
|
||||
prop="timestamp"
|
||||
min-width="180"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="tableColumns.find((col) => col.prop === 'created_time')?.show"
|
||||
label="创建时间"
|
||||
prop="created_time"
|
||||
min-width="180"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="tableColumns.find((col) => col.prop === 'updated_time')?.show"
|
||||
label="更新时间"
|
||||
prop="updated_time"
|
||||
min-width="180"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="tableColumns.find((col) => col.prop === 'operation')?.show"
|
||||
fixed="right"
|
||||
label="操作"
|
||||
align="center"
|
||||
min-width="180"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-hasPerm="['module_example:demo:detail']"
|
||||
type="info"
|
||||
size="small"
|
||||
link
|
||||
icon="document"
|
||||
@click="handleOpenDialog('detail', scope.row.id)"
|
||||
>
|
||||
详情
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPerm="['module_example:demo:update']"
|
||||
type="primary"
|
||||
size="small"
|
||||
link
|
||||
icon="edit"
|
||||
@click="handleOpenDialog('update', scope.row.id)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPerm="['module_example:demo:delete']"
|
||||
type="danger"
|
||||
size="small"
|
||||
link
|
||||
icon="delete"
|
||||
@click="handleDelete([scope.row.id])"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<template #footer>
|
||||
<pagination
|
||||
v-model:total="total"
|
||||
v-model:page="queryFormData.page_no"
|
||||
v-model:limit="queryFormData.page_size"
|
||||
@pagination="loadingData"
|
||||
/>
|
||||
</template>
|
||||
</el-card>
|
||||
|
||||
<!-- 弹窗区域 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible.visible"
|
||||
:title="dialogVisible.title"
|
||||
@close="handleCloseDialog"
|
||||
>
|
||||
<!-- 详情 -->
|
||||
<template v-if="dialogVisible.type === 'detail'">
|
||||
<el-descriptions :column="4" border>
|
||||
<el-descriptions-item label="会话ID" :span="2">
|
||||
{{ detailFormData.session_id }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="类型" :span="2">
|
||||
<el-tag :type="detailFormData.type === 'user' ? 'primary' : 'success'">
|
||||
{{ detailFormData.type === "user" ? "用户" : "助手" }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="内容" :span="4">
|
||||
<div v-if="detailFormData.content">
|
||||
<MarkdownRenderer :content="detailFormData.content" />
|
||||
</div>
|
||||
<div v-else class="empty-content">暂无内容</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="时间戳" :span="2">
|
||||
{{ detailFormData.timestamp }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" :span="2">
|
||||
{{ detailFormData.created_time }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间" :span="2">
|
||||
{{ detailFormData.updated_time }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
<!-- 新增、编辑表单 -->
|
||||
<template v-else>
|
||||
<el-form
|
||||
ref="dataFormRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
label-suffix=":"
|
||||
label-width="auto"
|
||||
label-position="right"
|
||||
inline
|
||||
>
|
||||
<el-form-item label="会话ID" prop="session_id">
|
||||
<el-input-number v-model="formData.session_id" placeholder="请输入会话ID" />
|
||||
</el-form-item>
|
||||
<el-form-item label="类型" prop="type">
|
||||
<el-radio-group v-model="formData.type">
|
||||
<el-radio value="user">用户</el-radio>
|
||||
<el-radio value="assistant">助手</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="内容" prop="content">
|
||||
<WangEditor v-model="formData.content" height="200px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="时间戳" prop="timestamp">
|
||||
<el-input-number v-model="formData.timestamp" placeholder="请输入时间戳" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<!-- 详情弹窗不需要确定按钮的提交逻辑 -->
|
||||
<el-button @click="handleCloseDialog">取消</el-button>
|
||||
<el-button v-if="dialogVisible.type !== 'detail'" type="primary" @click="handleSubmit">
|
||||
确定
|
||||
</el-button>
|
||||
<el-button v-else type="primary" @click="handleCloseDialog">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: "ChatMessage",
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { ElMessageBox } from "element-plus";
|
||||
import AiChatMessageAPI, {
|
||||
MessageTable,
|
||||
MessageForm,
|
||||
MessagePageQuery,
|
||||
} from "@/api/module_ai/chat_message";
|
||||
import { QuestionFilled, ArrowUp, ArrowDown } from "@element-plus/icons-vue";
|
||||
import { formatToDateTime } from "@/utils/dateUtil";
|
||||
import MarkdownRenderer from "@/components/MarkdownRenderer/index.vue";
|
||||
import WangEditor from "@/components/WangEditor/index.vue";
|
||||
|
||||
const visible = ref(true);
|
||||
const queryFormRef = ref();
|
||||
const dataFormRef = ref();
|
||||
const total = ref(0);
|
||||
const selectIds = ref<number[]>([]);
|
||||
const selectionRows = ref<MessageTable[]>([]);
|
||||
const loading = ref(false);
|
||||
const isExpand = ref(false);
|
||||
const isExpandable = ref(true);
|
||||
|
||||
// 日期范围临时变量
|
||||
const createdDateRange = ref<[Date, Date] | []>([]);
|
||||
|
||||
// 处理创建时间范围变化
|
||||
function handleCreatedDateRangeChange(range: [Date, Date]) {
|
||||
createdDateRange.value = range;
|
||||
if (range && range.length === 2) {
|
||||
queryFormData.created_time = [formatToDateTime(range[0]), formatToDateTime(range[1])];
|
||||
} else {
|
||||
queryFormData.created_time = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 分页表单
|
||||
const pageTableData = ref<MessageTable[]>([]);
|
||||
|
||||
// 表格列配置
|
||||
const tableColumns = ref([
|
||||
{ prop: "selection", label: "选择框", show: true },
|
||||
{ prop: "index", label: "序号", show: true },
|
||||
{ prop: "session_id", label: "会话ID", show: true },
|
||||
{ prop: "type", label: "类型", show: true },
|
||||
{ prop: "content", label: "内容", show: true },
|
||||
{ prop: "timestamp", label: "时间戳", show: true },
|
||||
{ prop: "created_time", label: "创建时间", show: true },
|
||||
{ prop: "updated_time", label: "更新时间", show: true },
|
||||
{ prop: "operation", label: "操作", show: true },
|
||||
]);
|
||||
|
||||
// 详情表单
|
||||
const detailFormData = ref<MessageTable>({
|
||||
session_id: 0,
|
||||
type: "user",
|
||||
content: "",
|
||||
timestamp: 0,
|
||||
});
|
||||
|
||||
// 分页查询参数
|
||||
const queryFormData = reactive<MessagePageQuery>({
|
||||
page_no: 1,
|
||||
page_size: 10,
|
||||
session_id: undefined,
|
||||
type: undefined,
|
||||
content: undefined,
|
||||
created_time: undefined,
|
||||
});
|
||||
|
||||
// 编辑表单
|
||||
const formData = reactive<MessageForm>({
|
||||
id: undefined,
|
||||
session_id: undefined,
|
||||
type: "user",
|
||||
content: "",
|
||||
timestamp: undefined,
|
||||
});
|
||||
|
||||
// 弹窗状态
|
||||
const dialogVisible = reactive({
|
||||
title: "",
|
||||
visible: false,
|
||||
type: "create" as "create" | "update" | "detail",
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
session_id: [{ required: true, message: "请输入会话ID", trigger: "blur" }],
|
||||
type: [{ required: true, message: "请选择类型", trigger: "change" }],
|
||||
content: [{ required: true, message: "请输入内容", trigger: "blur" }],
|
||||
});
|
||||
|
||||
// 列表刷新
|
||||
async function handleRefresh() {
|
||||
await loadingData();
|
||||
}
|
||||
|
||||
// 加载表格数据
|
||||
async function loadingData() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await AiChatMessageAPI.listMessage(queryFormData);
|
||||
pageTableData.value = response.data.data.items;
|
||||
total.value = response.data.data.total;
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 查询(重置页码后获取数据)
|
||||
async function handleQuery() {
|
||||
queryFormData.page_no = 1;
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
async function handleResetQuery() {
|
||||
queryFormRef.value.resetFields();
|
||||
queryFormData.page_no = 1;
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 定义初始表单数据常量
|
||||
const initialFormData: MessageForm = {
|
||||
id: undefined,
|
||||
session_id: undefined,
|
||||
type: "user",
|
||||
content: "",
|
||||
timestamp: undefined,
|
||||
};
|
||||
|
||||
// 重置表单
|
||||
async function resetForm() {
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.resetFields();
|
||||
dataFormRef.value.clearValidate();
|
||||
}
|
||||
// 完全重置 formData 为初始状态
|
||||
Object.assign(formData, initialFormData);
|
||||
}
|
||||
|
||||
// 行复选框选中项变化
|
||||
async function handleSelectionChange(selection: any) {
|
||||
selectIds.value = selection.map((item: any) => item.id);
|
||||
selectionRows.value = selection;
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
async function handleCloseDialog() {
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
// 打开弹窗
|
||||
async function handleOpenDialog(type: "create" | "update" | "detail", id?: number) {
|
||||
resetForm();
|
||||
dialogVisible.type = type;
|
||||
if (id) {
|
||||
const response = await AiChatMessageAPI.detailMessage(id);
|
||||
console.log("详情数据:", response.data.data);
|
||||
if (type === "detail") {
|
||||
dialogVisible.title = "详情";
|
||||
detailFormData.value = { ...response.data.data };
|
||||
console.log("detailFormData.value:", detailFormData.value);
|
||||
} else if (type === "update") {
|
||||
dialogVisible.title = "修改";
|
||||
Object.assign(formData, response.data.data);
|
||||
}
|
||||
} else {
|
||||
dialogVisible.title = "新增消息";
|
||||
formData.id = undefined;
|
||||
}
|
||||
dialogVisible.visible = true;
|
||||
}
|
||||
|
||||
// 提交表单(防抖)
|
||||
async function handleSubmit() {
|
||||
dataFormRef.value.validate(async (valid: any) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
const submitData = { ...formData };
|
||||
const id = formData.id;
|
||||
if (id) {
|
||||
try {
|
||||
await AiChatMessageAPI.updateMessage(id, { id, ...submitData });
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
handleCloseDialog();
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await AiChatMessageAPI.createMessage(submitData);
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
handleCloseDialog();
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 删除、批量删除
|
||||
async function handleDelete(ids: number[]) {
|
||||
ElMessageBox.confirm("确认删除该项数据?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await AiChatMessageAPI.deleteMessage(ids);
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessageBox.close();
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadingData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.empty-content {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
+304
-111
@@ -6,7 +6,7 @@
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>
|
||||
对话会话列表
|
||||
会话记忆
|
||||
<el-tooltip content="对话会话列表">
|
||||
<QuestionFilled class="w-4 h-4 mx-1" />
|
||||
</el-tooltip>
|
||||
@@ -83,7 +83,7 @@
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
v-hasPerm="['module_ai:chat_session:create']"
|
||||
v-hasPerm="['module_ai:chat:create']"
|
||||
type="success"
|
||||
icon="plus"
|
||||
@click="handleOpenDialog('create')"
|
||||
@@ -93,7 +93,7 @@
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
v-hasPerm="['module_ai:chat_session:delete']"
|
||||
v-hasPerm="['module_ai:chat:delete']"
|
||||
type="danger"
|
||||
icon="delete"
|
||||
:disabled="selectIds.length === 0"
|
||||
@@ -120,7 +120,7 @@
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="刷新">
|
||||
<el-button
|
||||
v-hasPerm="['module_ai:chat_session:query']"
|
||||
v-hasPerm="['module_ai:chat:query']"
|
||||
type="primary"
|
||||
icon="refresh"
|
||||
circle
|
||||
@@ -164,11 +164,72 @@
|
||||
{{ (queryFormData.page_no - 1) * queryFormData.page_size + scope.$index + 1 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="tableColumns.find((col) => col.prop === 'session_id')?.show"
|
||||
label="会话ID"
|
||||
prop="session_id"
|
||||
min-width="180"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="tableColumns.find((col) => col.prop === 'title')?.show"
|
||||
label="标题"
|
||||
prop="title"
|
||||
min-width="200"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-input
|
||||
v-if="editingRowId === scope.row.id"
|
||||
ref="titleInputRef"
|
||||
v-model="editingTitle"
|
||||
size="small"
|
||||
@blur="handleSaveTitle(scope.row)"
|
||||
@keyup.enter="handleSaveTitle(scope.row)"
|
||||
/>
|
||||
<span v-else class="editable-cell" title="点击编辑" @click="handleEditTitle(scope.row)">
|
||||
{{ scope.row.title || "未命名会话" }}
|
||||
<el-icon class="edit-icon"><Edit /></el-icon>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="tableColumns.find((col) => col.prop === 'user_id')?.show"
|
||||
label="用户ID"
|
||||
prop="user_id"
|
||||
min-width="120"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="tableColumns.find((col) => col.prop === 'team_id')?.show"
|
||||
label="团队ID"
|
||||
prop="team_id"
|
||||
min-width="120"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="tableColumns.find((col) => col.prop === 'team_name')?.show"
|
||||
label="部门名称"
|
||||
prop="team_name"
|
||||
min-width="120"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="tableColumns.find((col) => col.prop === 'agent_id')?.show"
|
||||
label="Agent ID"
|
||||
prop="agent_id"
|
||||
min-width="120"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="tableColumns.find((col) => col.prop === 'summary')?.show"
|
||||
label="会话摘要"
|
||||
prop="summary"
|
||||
min-width="200"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="tableColumns.find((col) => col.prop === 'message_count')?.show"
|
||||
label="消息数量"
|
||||
prop="message_count"
|
||||
min-width="100"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="tableColumns.find((col) => col.prop === 'created_time')?.show"
|
||||
@@ -182,36 +243,16 @@
|
||||
prop="updated_time"
|
||||
min-width="180"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="tableColumns.find((col) => col.prop === 'created_id')?.show"
|
||||
label="创建人"
|
||||
prop="created_id"
|
||||
min-width="120"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-tag>{{ scope.row.created_by?.name }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="tableColumns.find((col) => col.prop === 'updated_id')?.show"
|
||||
label="更新人"
|
||||
prop="updated_id"
|
||||
min-width="120"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-tag>{{ scope.row.updated_by?.name }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="tableColumns.find((col) => col.prop === 'operation')?.show"
|
||||
fixed="right"
|
||||
label="操作"
|
||||
align="center"
|
||||
min-width="180"
|
||||
min-width="120"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-hasPerm="['module_example:demo:detail']"
|
||||
v-hasPerm="['module_ai:chat:detail']"
|
||||
type="info"
|
||||
size="small"
|
||||
link
|
||||
@@ -221,17 +262,7 @@
|
||||
详情
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPerm="['module_example:demo:update']"
|
||||
type="primary"
|
||||
size="small"
|
||||
link
|
||||
icon="edit"
|
||||
@click="handleOpenDialog('update', scope.row.id)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPerm="['module_example:demo:delete']"
|
||||
v-hasPerm="['module_ai:chat:delete']"
|
||||
type="danger"
|
||||
size="small"
|
||||
link
|
||||
@@ -259,29 +290,75 @@
|
||||
<el-dialog
|
||||
v-model="dialogVisible.visible"
|
||||
:title="dialogVisible.title"
|
||||
class="session-detail-dialog"
|
||||
@close="handleCloseDialog"
|
||||
>
|
||||
<!-- 详情 -->
|
||||
<template v-if="dialogVisible.type === 'detail'">
|
||||
<el-descriptions :column="4" border>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="会话ID" :span="2">
|
||||
{{ detailFormData.session_id }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="标题" :span="2">
|
||||
{{ detailFormData.title }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建人" :span="2">
|
||||
{{ detailFormData.created_by?.name }}
|
||||
<el-descriptions-item label="用户ID" :span="1">
|
||||
{{ detailFormData.user_id }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新人" :span="2">
|
||||
{{ detailFormData.updated_by?.name }}
|
||||
<el-descriptions-item label="团队ID" :span="1">
|
||||
{{ detailFormData.team_id }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" :span="2">
|
||||
<el-descriptions-item label="部门名称" :span="1">
|
||||
{{ detailFormData.team_name || "未知部门" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="Agent ID" :span="1">
|
||||
{{ detailFormData.agent_id }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" :span="1">
|
||||
{{ detailFormData.created_time }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间" :span="2">
|
||||
<el-descriptions-item label="更新时间" :span="1">
|
||||
{{ detailFormData.updated_time }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="消息数量" :span="1">
|
||||
{{ detailFormData.message_count }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="会话摘要" :span="2">
|
||||
{{ detailFormData.summary || "无" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="元数据" :span="2">
|
||||
<pre v-if="detailFormData.metadata">{{
|
||||
JSON.stringify(detailFormData.metadata, null, 2)
|
||||
}}</pre>
|
||||
<span v-else>无</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<el-divider content-position="left">消息记录</el-divider>
|
||||
<el-timeline v-if="detailFormData.messages && detailFormData.messages.length > 0">
|
||||
<el-timeline-item
|
||||
v-for="(msg, index) in detailFormData.messages"
|
||||
:key="index"
|
||||
:type="msg.role === 'user' ? 'primary' : 'success'"
|
||||
:icon="msg.role === 'user' ? 'User' : 'ChatDotRound'"
|
||||
>
|
||||
<div class="message-item">
|
||||
<div class="message-header">
|
||||
<el-tag size="small" :type="msg.role === 'user' ? 'primary' : 'success'">
|
||||
{{ msg.role === "user" ? "用户" : "助手" }}
|
||||
</el-tag>
|
||||
<span v-if="msg.created_at" class="message-time">
|
||||
{{ formatTime(msg.created_at) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="message-content">{{ msg.content }}</div>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-empty v-else description="暂无消息记录" :image-size="60" />
|
||||
</template>
|
||||
<!-- 新增、编辑表单 -->
|
||||
<!-- 新增表单 -->
|
||||
<template v-else>
|
||||
<el-form
|
||||
ref="dataFormRef"
|
||||
@@ -318,44 +395,72 @@ defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { ElMessageBox } from "element-plus";
|
||||
import AiChatSessionAPI, {
|
||||
SessionTable,
|
||||
SessionForm,
|
||||
SessionPageQuery,
|
||||
} from "@/api/module_ai/chat_session";
|
||||
import { ref, reactive, onMounted, nextTick } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import AiChatAPI, { ChatSession } from "@/api/module_ai/chat";
|
||||
import DatePicker from "@/components/DatePicker/index.vue";
|
||||
import { QuestionFilled, ArrowUp, ArrowDown } from "@element-plus/icons-vue";
|
||||
import { QuestionFilled, ArrowUp, ArrowDown, Edit } from "@element-plus/icons-vue";
|
||||
import { formatToDateTime } from "@/utils/dateUtil";
|
||||
|
||||
const visible = ref(true);
|
||||
const queryFormRef = ref();
|
||||
const dataFormRef = ref();
|
||||
const titleInputRef = ref();
|
||||
const total = ref(0);
|
||||
const selectIds = ref<number[]>([]);
|
||||
const selectionRows = ref<SessionTable[]>([]);
|
||||
const selectIds = ref<string[]>([]);
|
||||
const selectionRows = ref<ChatSession[]>([]);
|
||||
const loading = ref(false);
|
||||
const isExpand = ref(false);
|
||||
const isExpandable = ref(true);
|
||||
|
||||
// 编辑标题相关
|
||||
const editingRowId = ref<string | null>(null);
|
||||
const editingTitle = ref("");
|
||||
|
||||
// 分页表单
|
||||
const pageTableData = ref<SessionTable[]>([]);
|
||||
const pageTableData = ref<ChatSession[]>([]);
|
||||
|
||||
// 表格列配置
|
||||
const tableColumns = ref([
|
||||
{ prop: "selection", label: "选择框", show: true },
|
||||
{ prop: "index", label: "序号", show: true },
|
||||
{ prop: "session_id", label: "会话ID", show: true },
|
||||
{ prop: "title", label: "标题", show: true },
|
||||
{ prop: "user_id", label: "用户ID", show: false },
|
||||
{ prop: "team_id", label: "团队ID", show: false },
|
||||
{ prop: "team_name", label: "部门名称", show: true },
|
||||
{ prop: "agent_id", label: "Agent ID", show: false },
|
||||
{ prop: "summary", label: "会话摘要", show: false },
|
||||
{ prop: "message_count", label: "消息数量", show: true },
|
||||
{ prop: "created_time", label: "创建时间", show: true },
|
||||
{ prop: "updated_time", label: "更新时间", show: true },
|
||||
{ prop: "created_id", label: "创建人", show: true },
|
||||
{ prop: "updated_id", label: "更新人", show: true },
|
||||
{ prop: "operation", label: "操作", show: true },
|
||||
]);
|
||||
|
||||
// 详情表单
|
||||
const detailFormData = ref<SessionTable>({ title: "" });
|
||||
const detailFormData = ref<ChatSession>({
|
||||
session_id: "",
|
||||
agent_id: null,
|
||||
team_id: null,
|
||||
team_name: null,
|
||||
workflow_id: null,
|
||||
user_id: null,
|
||||
session_data: null,
|
||||
agent_data: null,
|
||||
team_data: null,
|
||||
workflow_data: null,
|
||||
metadata: null,
|
||||
runs: null,
|
||||
summary: null,
|
||||
created_at: null,
|
||||
updated_at: null,
|
||||
id: "",
|
||||
title: null,
|
||||
created_time: null,
|
||||
updated_time: null,
|
||||
message_count: 0,
|
||||
messages: [],
|
||||
});
|
||||
// 日期范围临时变量
|
||||
const createdDateRange = ref<[Date, Date] | []>([]);
|
||||
// 更新时间范围临时变量
|
||||
@@ -365,9 +470,9 @@ const updatedDateRange = ref<[Date, Date] | []>([]);
|
||||
function handleCreatedDateRangeChange(range: [Date, Date]) {
|
||||
createdDateRange.value = range;
|
||||
if (range && range.length === 2) {
|
||||
queryFormData.created_time = [formatToDateTime(range[0]), formatToDateTime(range[1])];
|
||||
queryFormData.created_at = [formatToDateTime(range[0]), formatToDateTime(range[1])];
|
||||
} else {
|
||||
queryFormData.created_time = undefined;
|
||||
queryFormData.created_at = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,25 +480,24 @@ function handleCreatedDateRangeChange(range: [Date, Date]) {
|
||||
function handleUpdatedDateRangeChange(range: [Date, Date]) {
|
||||
updatedDateRange.value = range;
|
||||
if (range && range.length === 2) {
|
||||
queryFormData.updated_time = [formatToDateTime(range[0]), formatToDateTime(range[1])];
|
||||
queryFormData.updated_at = [formatToDateTime(range[0]), formatToDateTime(range[1])];
|
||||
} else {
|
||||
queryFormData.updated_time = undefined;
|
||||
queryFormData.updated_at = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 分页查询参数
|
||||
const queryFormData = reactive<SessionPageQuery>({
|
||||
const queryFormData = reactive({
|
||||
page_no: 1,
|
||||
page_size: 10,
|
||||
title: undefined,
|
||||
status: undefined,
|
||||
created_time: undefined,
|
||||
updated_time: undefined,
|
||||
created_at: undefined as string[] | undefined,
|
||||
updated_at: undefined as string[] | undefined,
|
||||
});
|
||||
|
||||
// 编辑表单
|
||||
const formData = reactive<SessionForm>({
|
||||
id: undefined,
|
||||
const formData = reactive({
|
||||
id: undefined as string | undefined,
|
||||
title: "",
|
||||
});
|
||||
|
||||
@@ -401,7 +505,7 @@ const formData = reactive<SessionForm>({
|
||||
const dialogVisible = reactive({
|
||||
title: "",
|
||||
visible: false,
|
||||
type: "create" as "create" | "update" | "detail",
|
||||
type: "create" as "create" | "detail",
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
@@ -409,6 +513,12 @@ const rules = reactive({
|
||||
title: [{ required: true, message: "请输入标题", trigger: "blur" }],
|
||||
});
|
||||
|
||||
// 格式化时间
|
||||
function formatTime(timestamp: number | null): string {
|
||||
if (!timestamp) return "";
|
||||
return formatToDateTime(new Date(timestamp * 1000));
|
||||
}
|
||||
|
||||
// 列表刷新
|
||||
async function handleRefresh() {
|
||||
await loadingData();
|
||||
@@ -418,9 +528,11 @@ async function handleRefresh() {
|
||||
async function loadingData() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await AiChatSessionAPI.listSession(queryFormData);
|
||||
pageTableData.value = response.data.data.items;
|
||||
total.value = response.data.data.total;
|
||||
const response = await AiChatAPI.getSessionList(queryFormData);
|
||||
const items = response.data.data?.items;
|
||||
const validItems = Array.isArray(items) ? items : [];
|
||||
pageTableData.value = validItems;
|
||||
total.value = response.data.data?.total || 0;
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
@@ -441,14 +553,14 @@ async function handleResetQuery() {
|
||||
// 重置日期范围选择器
|
||||
createdDateRange.value = [];
|
||||
updatedDateRange.value = [];
|
||||
queryFormData.created_time = undefined;
|
||||
queryFormData.updated_time = undefined;
|
||||
queryFormData.created_at = undefined;
|
||||
queryFormData.updated_at = undefined;
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 定义初始表单数据常量
|
||||
const initialFormData: SessionForm = {
|
||||
id: undefined,
|
||||
const initialFormData = {
|
||||
id: undefined as string | undefined,
|
||||
title: "",
|
||||
};
|
||||
|
||||
@@ -475,17 +587,15 @@ async function handleCloseDialog() {
|
||||
}
|
||||
|
||||
// 打开弹窗
|
||||
async function handleOpenDialog(type: "create" | "update" | "detail", id?: number) {
|
||||
async function handleOpenDialog(type: "create" | "detail", id?: string) {
|
||||
resetForm();
|
||||
dialogVisible.type = type;
|
||||
if (id) {
|
||||
const response = await AiChatSessionAPI.detailSession(id);
|
||||
const response = await AiChatAPI.getSessionDetail(id);
|
||||
if (type === "detail") {
|
||||
dialogVisible.title = "详情";
|
||||
Object.assign(detailFormData.value, response.data.data);
|
||||
} else if (type === "update") {
|
||||
dialogVisible.title = "修改";
|
||||
Object.assign(formData, response.data.data);
|
||||
const sessionData = response.data.data || {};
|
||||
Object.assign(detailFormData.value, sessionData);
|
||||
}
|
||||
} else {
|
||||
dialogVisible.title = "新增会话";
|
||||
@@ -494,44 +604,67 @@ async function handleOpenDialog(type: "create" | "update" | "detail", id?: numbe
|
||||
dialogVisible.visible = true;
|
||||
}
|
||||
|
||||
// 处理编辑标题
|
||||
function handleEditTitle(row: ChatSession) {
|
||||
editingRowId.value = row.id;
|
||||
editingTitle.value = row.title || "";
|
||||
nextTick(() => {
|
||||
titleInputRef.value?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
// 处理保存标题
|
||||
async function handleSaveTitle(row: ChatSession) {
|
||||
if (editingRowId.value !== row.id) return;
|
||||
|
||||
const newTitle = editingTitle.value.trim();
|
||||
if (!newTitle) {
|
||||
ElMessage.warning("标题不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
if (newTitle === row.title) {
|
||||
editingRowId.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
await AiChatAPI.updateSession(row.id, { title: newTitle });
|
||||
ElMessage.success("更新成功");
|
||||
row.title = newTitle;
|
||||
editingRowId.value = null;
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
ElMessage.error("更新失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单(防抖)
|
||||
async function handleSubmit() {
|
||||
dataFormRef.value.validate(async (valid: any) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
const submitData = { ...formData };
|
||||
const id = formData.id;
|
||||
if (id) {
|
||||
try {
|
||||
await AiChatSessionAPI.updateSession(id, { id, ...submitData });
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
handleCloseDialog();
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await AiChatSessionAPI.createSession(submitData);
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
handleCloseDialog();
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
try {
|
||||
await AiChatAPI.createSession({ title: submitData.title });
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
handleCloseDialog();
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 删除、批量删除
|
||||
async function handleDelete(ids: number[]) {
|
||||
async function handleDelete(ids: string[]) {
|
||||
ElMessageBox.confirm("确认删除该项数据?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
@@ -540,7 +673,7 @@ async function handleDelete(ids: number[]) {
|
||||
.then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await AiChatSessionAPI.deleteSession(ids);
|
||||
await AiChatAPI.deleteSession(ids);
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
@@ -558,4 +691,64 @@ onMounted(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss" scoped>
|
||||
.edit-icon {
|
||||
font-size: 12px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.editable-cell {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
|
||||
.edit-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-item {
|
||||
.message-header {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.message-content {
|
||||
padding: 8px 12px;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
max-height: 200px;
|
||||
padding: 8px;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
font-size: 12px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
// 弹窗样式 - 滚动条在弹窗内
|
||||
:deep(.session-detail-dialog .el-dialog__body) {
|
||||
max-height: 60vh;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user