Commit 2fe9c68a authored by ligaowei's avatar ligaowei

Merge branch 'feature/chat-form' into 'main'

1

See merge request !1
parents 8bcd41e5 8a629996
...@@ -202,6 +202,9 @@ frontend/.env.development.local ...@@ -202,6 +202,9 @@ frontend/.env.development.local
frontend/.env.test.local frontend/.env.test.local
frontend/.env.production.local frontend/.env.production.local
# Kiro IDE files
.kiro/
# OS generated files # OS generated files
Thumbs.db Thumbs.db
.DS_Store .DS_Store
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
"highlight.js": "^11.9.0", "highlight.js": "^11.9.0",
"marked": "^17.0.1", "marked": "^17.0.1",
"pako": "^2.1.0", "pako": "^2.1.0",
"pangea-ui": "^0.14.2-beta.9",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"snabbdom": "^3.6.3", "snabbdom": "^3.6.3",
"vue": "^3.4.0", "vue": "^3.4.0",
...@@ -26,11 +27,13 @@ ...@@ -26,11 +27,13 @@
"vue-router": "^4.3.0" "vue-router": "^4.3.0"
}, },
"devDependencies": { "devDependencies": {
"@arco-design/web-vue": "^2.55.3",
"@types/dompurify": "^3.0.5", "@types/dompurify": "^3.0.5",
"@types/node": "^20.10.0", "@types/node": "^20.10.0",
"@vitejs/plugin-vue": "^5.0.0", "@vitejs/plugin-vue": "^5.0.0",
"eslint": "^8.55.0", "eslint": "^8.55.0",
"eslint-plugin-vue": "^9.19.0", "eslint-plugin-vue": "^9.19.0",
"less": "^4.2.0",
"terser": "^5.44.1", "terser": "^5.44.1",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^5.0.0", "vite": "^5.0.0",
......
...@@ -2,13 +2,27 @@ ...@@ -2,13 +2,27 @@
<div class="chat-area"> <div class="chat-area">
<!-- 顶部Agent选择和操作栏 --> <!-- 顶部Agent选择和操作栏 -->
<div class="chat-header"> <div class="chat-header">
<el-select v-model="selectedAgent" @change="handleAgentChange" placeholder="选择智能体" class="agent-select"> <el-select
<el-option v-for="agent in agents" :key="agent.id" :label="agent.name" :value="agent.id"> v-model="selectedAgent"
@change="handleAgentChange"
placeholder="选择智能体"
class="agent-select"
>
<el-option
v-for="agent in agents"
:key="agent.id"
:label="agent.name"
:value="agent.id"
>
<span>{{ agent.name }} (ID: {{ agent.id }})</span> <span>{{ agent.name }} (ID: {{ agent.id }})</span>
</el-option> </el-option>
</el-select> </el-select>
<el-tooltip content="清空对话"> <el-tooltip content="清空对话">
<el-button @click="clearMessages" :disabled="messages.length === 0" circle> <el-button
@click="clearMessages"
:disabled="messages.length === 0"
circle
>
<span>🗑️</span> <span>🗑️</span>
</el-button> </el-button>
</el-tooltip> </el-tooltip>
...@@ -70,271 +84,300 @@ ...@@ -70,271 +84,300 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, nextTick, onMounted, defineExpose } from 'vue' import { ref, nextTick, onMounted, defineExpose } from "vue";
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from "element-plus";
import MessageItem from './MessageItem.vue' import MessageItem from "./MessageItem.vue";
import request from '@/utils/request' import request from "@/utils/request";
import { useRoute } from 'vue-router' import { useRoute } from "vue-router";
interface Message { interface Message {
content: string content: string;
isUser: boolean isUser: boolean;
agentId?: string agentId?: string;
timestamp: number timestamp: number;
isStreaming: boolean isStreaming: boolean;
hasError?: boolean hasError?: boolean;
originalMessage?: string originalMessage?: string;
} }
interface Agent { interface Agent {
id: string id: string;
name: string name: string;
[key: string]: any [key: string]: any;
} }
const selectedAgent = ref<string>('') const selectedAgent = ref<string>("");
const agents = ref<Agent[]>([]) const agents = ref<Agent[]>([]);
const messages = ref<Message[]>([]) const messages = ref<Message[]>([]);
const inputMessage = ref('') const inputMessage = ref("");
const isLoading = ref(false) const isLoading = ref(false);
const messagesContainer = ref<HTMLElement>() const messagesContainer = ref<HTMLElement>();
// 获取当前路由 // 获取当前路由
const route = useRoute() const route = useRoute();
// 全局维护SSE流超时计时器引用,确保能够正确清除 // 全局维护SSE流超时计时器引用,确保能够正确清除
let streamTimeoutTimer: ReturnType<typeof setTimeout> | null = null let streamTimeoutTimer: ReturnType<typeof setTimeout> | null = null;
// 获取Agent列表 // 获取Agent列表
const loadAgents = async () => { const loadAgents = async () => {
try { try {
const res = await request.get('/agent') const res = await request.get("/agent");
// 检查响应结构并正确提取数据 // 检查响应结构并正确提取数据
if (res && res.data && res.data.code === 200) { if (res && res.data && res.data.code === 200) {
agents.value = Array.isArray(res.data.data) ? res.data.data : [] agents.value = Array.isArray(res.data.data) ? res.data.data : [];
} else { } else {
agents.value = [] agents.value = [];
} }
console.log('[Agent列表加载] 获取到的Agent列表:', agents.value) console.log("[Agent列表加载] 获取到的Agent列表:", agents.value);
if (agents.value.length > 0 && !selectedAgent.value) { if (agents.value.length > 0 && !selectedAgent.value) {
selectedAgent.value = agents.value[0].id selectedAgent.value = agents.value[0].id;
} }
} catch (error) { } catch (error) {
console.error('获取Agent列表失败:', error) console.error("获取Agent列表失败:", error);
agents.value = [] agents.value = [];
ElMessage.error('获取Agent列表失败') ElMessage.error("获取Agent列表失败");
} }
} };
// 获取Agent名称 // 获取Agent名称
const getAgentName = (agentId?: string): string => { const getAgentName = (agentId?: string): string => {
if (!agentId) return 'Assistant' if (!agentId) return "Assistant";
const agent = agents.value.find(a => a.id === agentId) const agent = agents.value.find((a) => a.id === agentId);
return agent?.name || 'Assistant' return agent?.name || "Assistant";
} };
// 处理Agent切换 // 处理Agent切换
const handleAgentChange = () => { const handleAgentChange = () => {
clearMessages() clearMessages();
} };
// 清空消息 // 清空消息
const clearMessages = () => { const clearMessages = () => {
// 清除可能存在的超时计时器 // 清除可能存在的超时计时器
clearStreamTimeout() clearStreamTimeout();
ElMessageBox.confirm('确认清空所有对话吗?此操作无法撤销。', '提示', { ElMessageBox.confirm("确认清空所有对话吗?此操作无法撤销。", "提示", {
confirmButtonText: '清空', confirmButtonText: "清空",
cancelButtonText: '取消', cancelButtonText: "取消",
type: 'warning' type: "warning",
}).then(() => {
messages.value = []
ElMessage.success('对话已清空')
}).catch(() => {
// 用户取消
}) })
} .then(() => {
messages.value = [];
ElMessage.success("对话已清空");
})
.catch(() => {
// 用户取消
});
};
// 合并重复的历史消息加载逻辑 // 合并重复的历史消息加载逻辑
const loadHistoryMessagesInternal = async (agentId: string) => { const loadHistoryMessagesInternal = async (agentId: string) => {
try { try {
// 添加更严格的类型和存在性检查 // 添加更严格的类型和存在性检查
if (!agentId || typeof agentId !== 'string' || agentId.trim() === '') { if (!agentId || typeof agentId !== "string" || agentId.trim() === "") {
console.log('[历史消息加载] 没有指定有效的Agent ID,跳过加载历史记录') console.log("[历史消息加载] 没有指定有效的Agent ID,跳过加载历史记录");
messages.value = [] messages.value = [];
return return;
} }
// 验证agentId是否存在于可用agents列表中 // 验证agentId是否存在于可用agents列表中
const isValidAgent = agents.value.some(agent => agent.id === agentId.trim()) const isValidAgent = agents.value.some(
(agent) => agent.id === agentId.trim()
);
// 如果agents列表为空,我们仍然尝试加载历史消息,因为可能是初始化时agents还未加载完成 // 如果agents列表为空,我们仍然尝试加载历史消息,因为可能是初始化时agents还未加载完成
if (agents.value.length > 0 && !isValidAgent) { if (agents.value.length > 0 && !isValidAgent) {
console.log(`[历史消息加载] 指定的Agent ID '${agentId}' 不存在于可用agents列表中`) console.log(
`[历史消息加载] 指定的Agent ID '${agentId}' 不存在于可用agents列表中`
);
// 不立即返回,而是继续尝试加载历史消息 // 不立即返回,而是继续尝试加载历史消息
} }
// 设置选中的Agent // 设置选中的Agent
selectedAgent.value = agentId.trim() selectedAgent.value = agentId.trim();
// 调用后端API获取历史对话记录 // 调用后端API获取历史对话记录
console.log(`[历史消息加载] 正在请求API: /memory/dialogue/agent/${agentId.trim()}`) console.log(
const res = await request.get(`/memory/dialogue/agent/${encodeURIComponent(agentId.trim())}`) `[历史消息加载] 正在请求API: /memory/dialogue/agent/${agentId.trim()}`
);
const res = await request.get(
`/memory/dialogue/agent/${encodeURIComponent(agentId.trim())}`
);
console.log('[历史消息加载] API响应:', res) console.log("[历史消息加载] API响应:", res);
// 验证响应结构:包括code、data和messages数组 // 验证响应结构:包括code、data和messages数组
if (!res || !res.data) { if (!res || !res.data) {
console.error('[历史消息加载] 响应对象为空或无效') console.error("[历史消息加载] 响应对象为空或无效");
messages.value = [] messages.value = [];
return return;
} }
const { code, data } = res.data const { code, data } = res.data;
console.log(`[历史消息加载] 响应code: ${code}, data:`, data) console.log(`[历史消息加载] 响应code: ${code}, data:`, data);
if (code === 200 && data && typeof data === 'object') { if (code === 200 && data && typeof data === "object") {
const messagesArray = data.messages const messagesArray = data.messages;
// 检查messages是否是数组 // 检查messages是否是数组
if (!Array.isArray(messagesArray)) { if (!Array.isArray(messagesArray)) {
console.warn(`[历史消息加载] messages字段不是数组,类型为: ${typeof messagesArray},值为:`, messagesArray) console.warn(
messages.value = [] `[历史消息加载] messages字段不是数组,类型为: ${typeof messagesArray},值为:`,
return messagesArray
);
messages.value = [];
return;
} }
// 检查是否有消息 // 检查是否有消息
if (messagesArray.length === 0) { if (messagesArray.length === 0) {
console.log('[历史消息加载] 没有找到历史消息,API返回空数组') console.log("[历史消息加载] 没有找到历史消息,API返回空数组");
messages.value = [] messages.value = [];
return return;
} }
// 转换消息格式 // 转换消息格式
const historyMessages = messagesArray.map((msg: any) => { const historyMessages = messagesArray
.map((msg: any) => {
// 验证消息对象的必要字段 // 验证消息对象的必要字段
if (!msg || typeof msg !== 'object') { if (!msg || typeof msg !== "object") {
console.warn('[历史消息加载] 发现无效的消息对象:', msg) console.warn("[历史消息加载] 发现无效的消息对象:", msg);
return null return null;
} }
const sender = msg.sender const sender = msg.sender;
const content = msg.content const content = msg.content;
const time = msg.time const time = msg.time;
if (!content) { if (!content) {
console.warn('[历史消息加载] 消息内容为空:', msg) console.warn("[历史消息加载] 消息内容为空:", msg);
return null return null;
} }
return { return {
content: content, content: content,
isUser: sender === 'user', isUser: sender === "user",
agentId: agentId.trim(), agentId: agentId.trim(),
timestamp: time ? (typeof time === 'string' ? parseInt(time) : new Date(time).getTime()) : Date.now(), timestamp: time
isStreaming: false ? typeof time === "string"
} ? parseInt(time)
}).filter((msg: any) => msg !== null) // 过滤掉无效消息 : new Date(time).getTime()
: Date.now(),
isStreaming: false,
};
})
.filter((msg: any) => msg !== null); // 过滤掉无效消息
// 修复:确保消息按时间顺序排列(最新的消息在最后) // 修复:确保消息按时间顺序排列(最新的消息在最后)
historyMessages.sort((a: any, b: any) => a.timestamp - b.timestamp) historyMessages.sort((a: any, b: any) => a.timestamp - b.timestamp);
messages.value = historyMessages messages.value = historyMessages;
console.log(`[历史消息加载] 成功加载${historyMessages.length}条历史消息`) console.log(`[历史消息加载] 成功加载${historyMessages.length}条历史消息`);
// 滚动到底部 // 滚动到底部
await nextTick() await nextTick();
if (messagesContainer.value) { if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight messagesContainer.value.scrollTop =
messagesContainer.value.scrollHeight;
} }
} else { } else {
console.error(`[历史消息加载] 响应格式错误或错误状态。code: ${code}, data:`, data) console.error(
messages.value = [] `[历史消息加载] 响应格式错误或错误状态。code: ${code}, data:`,
data
);
messages.value = [];
} }
} catch (error: any) { } catch (error: any) {
console.error('[历史消息加载] 加载历史对话记录失败', error) console.error("[历史消息加载] 加载历史对话记录失败", error);
messages.value = [] // 出错时清空消息列表 messages.value = []; // 出错时清空消息列表
// 记录详细的错误信息便于调试 // 记录详细的错误信息便于调试
if (error.response) { if (error.response) {
console.error('[历史消息加载] HTTP响应错误 - 状态码:', error.response.status) console.error(
console.error('[历史消息加载] HTTP响应错误 - 数据:', error.response.data) "[历史消息加载] HTTP响应错误 - 状态码:",
console.error('[历史消息加载] HTTP响应错误 - 请求URL:', error.config?.url) error.response.status
);
console.error("[历史消息加载] HTTP响应错误 - 数据:", error.response.data);
console.error(
"[历史消息加载] HTTP响应错误 - 请求URL:",
error.config?.url
);
} else if (error.request) { } else if (error.request) {
console.error('[历史消息加载] 网络请求错误 - 没有收到响应') console.error("[历史消息加载] 网络请求错误 - 没有收到响应");
console.error('[历史消息加载] 请求配置:', error.request) console.error("[历史消息加载] 请求配置:", error.request);
} else { } else {
console.error('[历史消息加载] 错误信息:', error.message) console.error("[历史消息加载] 错误信息:", error.message);
console.error('[历史消息加载] 错误堆栈:', error.stack) console.error("[历史消息加载] 错误堆栈:", error.stack);
} }
} }
} };
// 加载历史对话记录 // 加载历史对话记录
const loadHistoryMessages = async () => { const loadHistoryMessages = async () => {
// 首先尝试从路由参数获取agentId // 首先尝试从路由参数获取agentId
let agentId = route.query.agentId as string let agentId = route.query.agentId as string;
// 如果路由参数中没有agentId,则使用下拉框中选中的值 // 如果路由参数中没有agentId,则使用下拉框中选中的值
if (!agentId || typeof agentId !== 'string' || agentId.trim() === '') { if (!agentId || typeof agentId !== "string" || agentId.trim() === "") {
agentId = selectedAgent.value agentId = selectedAgent.value;
} }
// 如果仍然没有有效的agentId,则不加载历史消息 // 如果仍然没有有效的agentId,则不加载历史消息
if (!agentId || typeof agentId !== 'string' || agentId.trim() === '') { if (!agentId || typeof agentId !== "string" || agentId.trim() === "") {
console.log('[历史消息加载] 没有指定有效的Agent ID,跳过加载历史记录') console.log("[历史消息加载] 没有指定有效的Agent ID,跳过加载历史记录");
return return;
} }
await loadHistoryMessagesInternal(agentId); await loadHistoryMessagesInternal(agentId);
} };
// 通过指定agentId加载历史对话记录(供父组件调用) // 通过指定agentId加载历史对话记录(供父组件调用)
const loadHistoryByAgentId = async (agentId: string) => { const loadHistoryByAgentId = async (agentId: string) => {
await loadHistoryMessagesInternal(agentId); await loadHistoryMessagesInternal(agentId);
} };
// 处理重试 // 处理重试
const handleRetry = async (index: number) => { const handleRetry = async (index: number) => {
// 清除可能存在的超时计时器 // 清除可能存在的超时计时器
clearStreamTimeout() clearStreamTimeout();
const msg = messages.value[index] const msg = messages.value[index];
if (!msg.hasError || msg.isUser) return if (!msg.hasError || msg.isUser) return;
// 移除错误消息 // 移除错误消息
messages.value.splice(index, 1) messages.value.splice(index, 1);
// 重新发送原始消息 // 重新发送原始消息
inputMessage.value = msg.originalMessage || '' inputMessage.value = msg.originalMessage || "";
await sendMessage() await sendMessage();
} };
// 统一的超时计时器清理函数 // 统一的超时计时器清理函数
const clearStreamTimeout = () => { const clearStreamTimeout = () => {
if (streamTimeoutTimer) { if (streamTimeoutTimer) {
clearTimeout(streamTimeoutTimer) clearTimeout(streamTimeoutTimer);
streamTimeoutTimer = null streamTimeoutTimer = null;
} }
} };
// 防抖函数 // 防抖函数
const debounce = (func: Function, wait: number) => { const debounce = (func: Function, wait: number) => {
let timeout: ReturnType<typeof setTimeout> let timeout: ReturnType<typeof setTimeout>;
return function executedFunction(...args: any[]) { return function executedFunction(...args: any[]) {
const later = () => { const later = () => {
clearTimeout(timeout) clearTimeout(timeout);
func(...args) func(...args);
} };
clearTimeout(timeout) clearTimeout(timeout);
timeout = setTimeout(later, wait) timeout = setTimeout(later, wait);
} };
} };
// 自动滚动到底部(使用防抖优化性能) // 自动滚动到底部(使用防抖优化性能)
const scrollToBottom = debounce(async () => { const scrollToBottom = debounce(async () => {
await nextTick() await nextTick();
if (messagesContainer.value) { if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
} }
}, 100) }, 100);
// 【修复工具函数】处理转义序列,将\n、\t等转义形式还原为实际字符 // 【修复工具函数】处理转义序列,将\n、\t等转义形式还原为实际字符
const unescapeString = (str: string): string => { const unescapeString = (str: string): string => {
...@@ -342,32 +385,40 @@ const unescapeString = (str: string): string => { ...@@ -342,32 +385,40 @@ const unescapeString = (str: string): string => {
try { try {
// 处理转义序列:将转义形式还原为实际字符 // 处理转义序列:将转义形式还原为实际字符
let unescaped = str let unescaped = str
.replace(/\\n/g, '\n') // \n 转换为换行符 .replace(/\\n/g, "\n") // \n 转换为换行符
.replace(/\\r/g, '\r') // \r 转换为回车符 .replace(/\\r/g, "\r") // \r 转换为回车符
.replace(/\\t/g, '\t') // \t 转换为制表符 .replace(/\\t/g, "\t") // \t 转换为制表符
.replace(/\\b/g, '\b') // \b 转换为退格符 .replace(/\\b/g, "\b") // \b 转换为退格符
.replace(/\\f/g, '\f') // \f 转换为换页符 .replace(/\\f/g, "\f") // \f 转换为换页符
.replace(/\\\\/g, '\\') // \\ 转换为单个反斜杠 .replace(/\\\\/g, "\\"); // \\ 转换为单个反斜杠
return unescaped; return unescaped;
} catch (e) { } catch (e) {
console.warn('转义序列处理失败:', e); console.warn("转义序列处理失败:", e);
return str; return str;
} }
}; };
// 处理SSE数据行的通用函数 // 处理SSE数据行的通用函数
const processSSELine = async (line: string, accumulatedContentRef: { value: string }, hasFinalAnswerRef: { value: boolean }, currentEventRef: { value: string }, aiMessageIndex: number, resetStreamTimeout: () => void) => { const processSSELine = async (
if (!line.trim()) return false line: string,
accumulatedContentRef: { value: string },
if (line.startsWith('event:')) { hasFinalAnswerRef: { value: boolean },
currentEventRef.value = line.slice(6).trim() currentEventRef: { value: string },
return false aiMessageIndex: number,
} else if (line.startsWith('data:')) { resetStreamTimeout: () => void
) => {
if (!line.trim()) return false;
console.log("line", line);
if (line.startsWith("event:")) {
currentEventRef.value = line.slice(6).trim();
return false;
} else if (line.startsWith("data:")) {
try { try {
const dataStr = line.startsWith('data: ') ? line.slice(6) : line.slice(5) const dataStr = line.startsWith("data: ") ? line.slice(6) : line.slice(5);
// 修复:检查dataStr是否为空或只包含空白字符 // 修复:检查dataStr是否为空或只包含空白字符
if (!dataStr.trim()) { if (!dataStr.trim()) {
return false return false;
} }
// 尝试解析为JSON,如果失败则作为纯文本处理 // 尝试解析为JSON,如果失败则作为纯文本处理
let data; let data;
...@@ -376,62 +427,73 @@ const processSSELine = async (line: string, accumulatedContentRef: { value: stri ...@@ -376,62 +427,73 @@ const processSSELine = async (line: string, accumulatedContentRef: { value: stri
} catch (e) { } catch (e) {
// 不是JSON格式,将其作为token事件处理(用于兼容旧协议) // 不是JSON格式,将其作为token事件处理(用于兼容旧协议)
data = { data = {
type: 'token', type: "token",
token: dataStr token: dataStr,
}; };
} }
// 修复:检查是否是JSON格式的API密钥错误消息 // 修复:检查是否是JSON格式的API密钥错误消息
if (typeof data === 'object' && data !== null) { if (typeof data === "object" && data !== null) {
// 检查是否包含API密钥错误信息 // 检查是否包含API密钥错误信息
const errorMessage = data.message || data.error || data.token || data.content || ''; const errorMessage =
if (errorMessage.includes('请配置API密钥')) { data.message || data.error || data.token || data.content || "";
if (errorMessage.includes("请配置API密钥")) {
// 处理API密钥错误 // 处理API密钥错误
clearStreamTimeout(); clearStreamTimeout();
messages.value[aiMessageIndex].isStreaming = false; messages.value[aiMessageIndex].isStreaming = false;
messages.value[aiMessageIndex].content = '[错误] 请配置API密钥'; messages.value[aiMessageIndex].content = "[错误] 请配置API密钥";
messages.value[aiMessageIndex].hasError = true; messages.value[aiMessageIndex].hasError = true;
isLoading.value = false; isLoading.value = false;
console.error('[SSE解析错误] 接收到API密钥错误消息:', dataStr); console.error("[SSE解析错误] 接收到API密钥错误消息:", dataStr);
return true; // 返回true表示流已完成 return true; // 返回true表示流已完成
} }
} }
// 特殊处理流结束标记 // 特殊处理流结束标记
if (dataStr.trim() === '[DONE]') { if (dataStr.trim() === "[DONE]") {
clearStreamTimeout(); clearStreamTimeout();
messages.value[aiMessageIndex].isStreaming = false; messages.value[aiMessageIndex].isStreaming = false;
isLoading.value = false; isLoading.value = false;
console.log('[SSE流结束] 收到[DONE]标记'); console.log("[SSE流结束] 收到[DONE]标记");
return true; // 返回true表示流已完成 return true; // 返回true表示流已完成
} }
// 检查是否是纯文本错误消息 // 检查是否是纯文本错误消息
// 修复:正确处理API密钥错误,检查是否为JSON格式的错误消息 // 修复:正确处理API密钥错误,检查是否为JSON格式的错误消息
if ((dataStr.startsWith('[错误]') || dataStr.includes('请配置API密钥')) && !dataStr.trim().startsWith('{')) { if (
(dataStr.startsWith("[错误]") || dataStr.includes("请配置API密钥")) &&
!dataStr.trim().startsWith("{")
) {
// 处理纯文本错误消息,包括API密钥错误 // 处理纯文本错误消息,包括API密钥错误
clearStreamTimeout(); clearStreamTimeout();
messages.value[aiMessageIndex].isStreaming = false; messages.value[aiMessageIndex].isStreaming = false;
messages.value[aiMessageIndex].content = dataStr.startsWith('[错误]') ? dataStr : '[错误] ' + dataStr; messages.value[aiMessageIndex].content = dataStr.startsWith("[错误]")
? dataStr
: "[错误] " + dataStr;
messages.value[aiMessageIndex].hasError = true; messages.value[aiMessageIndex].hasError = true;
isLoading.value = false; isLoading.value = false;
console.warn('[SSE解析错误] 接收到错误消息:', dataStr); console.warn("[SSE解析错误] 接收到错误消息:", dataStr);
return true; // 返回true表示流已完成 return true; // 返回true表示流已完成
} }
// 新增:处理JSON格式但包含API密钥错误的情况 // 新增:处理JSON格式但包含API密钥错误的情况
if (typeof data === 'object' && data !== null) { if (typeof data === "object" && data !== null) {
const jsonData = data; const jsonData = data;
// 检查各种可能包含错误信息的字段 // 检查各种可能包含错误信息的字段
const errorMsg = jsonData.message || jsonData.error || jsonData.token || jsonData.content || ''; const errorMsg =
if (errorMsg.includes('请配置API密钥')) { jsonData.message ||
jsonData.error ||
jsonData.token ||
jsonData.content ||
"";
if (errorMsg.includes("请配置API密钥")) {
// 处理API密钥错误 // 处理API密钥错误
clearStreamTimeout(); clearStreamTimeout();
messages.value[aiMessageIndex].isStreaming = false; messages.value[aiMessageIndex].isStreaming = false;
messages.value[aiMessageIndex].content = '[错误] 请配置API密钥'; messages.value[aiMessageIndex].content = "[错误] 请配置API密钥";
messages.value[aiMessageIndex].hasError = true; messages.value[aiMessageIndex].hasError = true;
isLoading.value = false; isLoading.value = false;
console.error('[SSE解析错误] 接收到API密钥错误消息:', dataStr); console.error("[SSE解析错误] 接收到API密钥错误消息:", dataStr);
return true; // 返回true表示流已完成 return true; // 返回true表示流已完成
} }
} }
...@@ -440,20 +502,20 @@ const processSSELine = async (line: string, accumulatedContentRef: { value: stri ...@@ -440,20 +502,20 @@ const processSSELine = async (line: string, accumulatedContentRef: { value: stri
// 根据事件类型处理数据 // 根据事件类型处理数据
switch (eventType) { switch (eventType) {
case 'token': case "token":
// 重置超时计时器,接收到token说明连接还活跃 // 重置超时计时器,接收到token说明连接还活跃
resetStreamTimeout(); resetStreamTimeout();
// 修复:对接收到的token进行反转义处理,䮵保换行符和格式化符号正常显示 // 修复:对接收到的token进行反转义处理,䮵保换行符和格式化符号正常显示
const processedToken = unescapeString(data.token || ''); const processedToken = unescapeString(data.token || "");
accumulatedContentRef.value += processedToken; accumulatedContentRef.value += processedToken;
messages.value[aiMessageIndex].content = accumulatedContentRef.value; messages.value[aiMessageIndex].content = accumulatedContentRef.value;
await scrollToBottom(); await scrollToBottom();
break; break;
case 'complete': case "complete":
// 收到完成事件,清除超时计时器 // 收到完成事件,清除超时计时器
clearStreamTimeout(); clearStreamTimeout();
console.log('[SSE完成事件]', data); console.log("[SSE完成事件]", data);
// 如果有完整文本内容,更新消息内容 // 如果有完整文本内容,更新消息内容
if (data.fullText) { if (data.fullText) {
messages.value[aiMessageIndex].content = data.fullText; messages.value[aiMessageIndex].content = data.fullText;
...@@ -463,43 +525,52 @@ const processSSELine = async (line: string, accumulatedContentRef: { value: stri ...@@ -463,43 +525,52 @@ const processSSELine = async (line: string, accumulatedContentRef: { value: stri
isLoading.value = false; isLoading.value = false;
return true; // 返回true表示流已完成 return true; // 返回true表示流已完成
case 'error': case "error":
// 收到错误事件,清除超时计时器 // 收到错误事件,清除超时计时器
clearStreamTimeout(); clearStreamTimeout();
messages.value[aiMessageIndex].isStreaming = false; messages.value[aiMessageIndex].isStreaming = false;
// 检查是否是API密钥错误的特殊提示 // 检查是否是API密钥错误的特殊提示
const errorMsg = data.message || data.error || data.token || data.content || ''; const errorMsg =
if (errorMsg.includes('请配置API密钥')) { data.message || data.error || data.token || data.content || "";
messages.value[aiMessageIndex].content = '[错误] 请配置API密钥'; if (errorMsg.includes("请配置API密钥")) {
messages.value[aiMessageIndex].content = "[错误] 请配置API密钥";
} else { } else {
messages.value[aiMessageIndex].content = `[错误] ${errorMsg || '未知错误'}`; messages.value[aiMessageIndex].content = `[错误] ${
errorMsg || "未知错误"
}`;
} }
messages.value[aiMessageIndex].hasError = true; messages.value[aiMessageIndex].hasError = true;
isLoading.value = false; isLoading.value = false;
// 记录错误日志便于调试 // 记录错误日志便于调试
console.error('[SSE错误事件]', data); console.error("[SSE错误事件]", data);
return true; // 返回true表示流已完成 return true; // 返回true表示流已完成
case 'thinking': case "thinking":
// 处理思考事件,将其发送到时间轴面板 // 处理思考事件,将其发送到时间轴面板
const event = { const event = {
type: 'thought', type: "thought",
title: data.thinkingType === 'final_answer' ? '最终答案' : '思考过程', title:
data.thinkingType === "final_answer" ? "最终答案" : "思考过程",
content: data.content, content: data.content,
timestamp: data.timestamp timestamp: data.timestamp,
}; };
// 通过事件总线将事件发送到时间轴 // 通过事件总线将事件发送到时间轴
window.dispatchEvent(new CustomEvent('timeline-event', { detail: event })); window.dispatchEvent(
new CustomEvent("timeline-event", { detail: event })
);
// 如果是最终答案,也应该显示在主要对话框中 // 如果是最终答案,也应该显示在主要对话框中
// 修复:确保最终答案只添加一次,避免重复显示 // 修复:确保最终答案只添加一次,避免重复显示
if (data.thinkingType === 'final_answer' && !hasFinalAnswerRef.value) { if (
data.thinkingType === "final_answer" &&
!hasFinalAnswerRef.value
) {
hasFinalAnswerRef.value = true; hasFinalAnswerRef.value = true;
// 修复:这里不应该再添加到accumulatedContentRef.value,因为这会在流结束后再次设置导致重复 // 修复:这里不应该再添加到accumulatedContentRef.value,因为这会在流结束后再次设置导致重复
// accumulatedContentRef.value += data.content || ''; // accumulatedContentRef.value += data.content || '';
// 直接设置消息内容为最终答案 // 直接设置消息内容为最终答案
messages.value[aiMessageIndex].content = data.content || ''; messages.value[aiMessageIndex].content = data.content || "";
messages.value[aiMessageIndex].isStreaming = false; messages.value[aiMessageIndex].isStreaming = false;
isLoading.value = false; isLoading.value = false;
await scrollToBottom(); await scrollToBottom();
...@@ -507,29 +578,35 @@ const processSSELine = async (line: string, accumulatedContentRef: { value: stri ...@@ -507,29 +578,35 @@ const processSSELine = async (line: string, accumulatedContentRef: { value: stri
// 对于非最终答案的思考过程,不添加到主对话框中 // 对于非最终答案的思考过程,不添加到主对话框中
break; break;
case 'tool_call': case "tool_call":
case 'embed': case "embed":
// 处理工具调用和嵌入事件,将其发送到时间轴面板 // 处理工具调用和嵌入事件,将其发送到时间轴面板
// 构建事件标题 // 构建事件标题
let title = data.title || '事件'; let title = data.title || "事件";
if (eventType === 'tool_call' && data.toolName) { if (eventType === "tool_call" && data.toolName) {
title = `调用工具: ${data.toolName}`; title = `调用工具: ${data.toolName}`;
} else if (eventType === 'embed' && data.embedTitle) { } else if (eventType === "embed" && data.embedTitle) {
title = data.embedTitle; title = data.embedTitle;
} }
// 构建元数据 // 构建元数据
const metadata: Record<string, any> = data.metadata || {}; const metadata: Record<string, any> = data.metadata || {};
if (eventType === 'tool_call') { if (eventType === "tool_call") {
if (data.toolName) metadata['工具'] = data.toolName; if (data.toolName) metadata["工具"] = data.toolName;
if (data.toolAction) metadata['操作'] = data.toolAction; if (data.toolAction) metadata["操作"] = data.toolAction;
if (data.toolInput) metadata['输入'] = JSON.stringify(data.toolInput).substring(0, 100); if (data.toolInput)
if (data.toolOutput) metadata['输出'] = String(data.toolOutput).substring(0, 100); metadata["输入"] = JSON.stringify(data.toolInput).substring(
if (data.toolStatus) metadata['状态'] = data.toolStatus; 0,
if (data.executionTime) metadata['耗时'] = `${data.executionTime}ms`; 100
} else if (eventType === 'embed') { );
if (data.embedUrl) metadata['URL'] = data.embedUrl; if (data.toolOutput)
if (data.embedType) metadata['类型'] = data.embedType; metadata["输出"] = String(data.toolOutput).substring(0, 100);
if (data.toolStatus) metadata["状态"] = data.toolStatus;
if (data.executionTime)
metadata["耗时"] = `${data.executionTime}ms`;
} else if (eventType === "embed") {
if (data.embedUrl) metadata["URL"] = data.embedUrl;
if (data.embedType) metadata["类型"] = data.embedType;
} }
const timelineEvent = { const timelineEvent = {
...@@ -547,52 +624,61 @@ const processSSELine = async (line: string, accumulatedContentRef: { value: stri ...@@ -547,52 +624,61 @@ const processSSELine = async (line: string, accumulatedContentRef: { value: stri
embedType: data.embedType, embedType: data.embedType,
embedTitle: data.embedTitle, embedTitle: data.embedTitle,
embedHtmlContent: data.embedHtmlContent, embedHtmlContent: data.embedHtmlContent,
timestamp: data.timestamp || Date.now() timestamp: data.timestamp || Date.now(),
}; };
// 通过事件总线将事件发送到时间轴 // 通过事件总线将事件发送到时间轴
console.log('[ChatArea] 发送timeline-event事件:', timelineEvent); console.log("[ChatArea] 发送timeline-event事件:", timelineEvent);
window.dispatchEvent(new CustomEvent('timeline-event', { detail: timelineEvent })); window.dispatchEvent(
new CustomEvent("timeline-event", { detail: timelineEvent })
);
// 对于embed事件,还需要触发embed-event事件 // 对于embed事件,还需要触发embed-event事件
if (eventType === 'embed' && data.embedUrl) { if (eventType === "embed" && data.embedUrl) {
window.dispatchEvent(new CustomEvent('embed-event', { window.dispatchEvent(
new CustomEvent("embed-event", {
detail: { detail: {
url: data.embedUrl, url: data.embedUrl,
type: data.embedType, type: data.embedType,
title: data.embedTitle, title: data.embedTitle,
htmlContent: data.embedHtmlContent htmlContent: data.embedHtmlContent,
} },
})); })
);
} }
break; break;
} }
// 重置当前事件类型 // 重置当前事件类型
currentEventRef.value = ''; currentEventRef.value = "";
} catch (err) { } catch (err) {
console.error('[SSE解析错误] 解析SSE数据失败,重置超时计时器:', err, '原始行:', line) console.error(
"[SSE解析错误] 解析SSE数据失败,重置超时计时器:",
err,
"原始行:",
line
);
// 收到任何消息,都要重置超时计时器 // 收到任何消息,都要重置超时计时器
resetStreamTimeout() resetStreamTimeout();
// 根据错误类型,决定是否继续处理或中断流 // 根据错误类型,决定是否继续处理或中断流
if (line.includes('"type":"error"') || line.includes('"error"')) { if (line.includes('"type":"error"') || line.includes('"error"')) {
// 错误消息,继续处理 // 错误消息,继续处理
return false return false;
} }
} }
return false return false;
} }
return false return false;
} };
// 发送消息 // 发送消息
const sendMessage = async () => { const sendMessage = async () => {
if (!selectedAgent.value || !inputMessage.value.trim()) { if (!selectedAgent.value || !inputMessage.value.trim()) {
return return;
} }
const userMessage = inputMessage.value.trim() const userMessage = inputMessage.value.trim();
inputMessage.value = '' inputMessage.value = "";
// 添加用户消息 // 添加用户消息
messages.value.push({ messages.value.push({
...@@ -600,121 +686,142 @@ const sendMessage = async () => { ...@@ -600,121 +686,142 @@ const sendMessage = async () => {
isUser: true, isUser: true,
agentId: selectedAgent.value, agentId: selectedAgent.value,
timestamp: Date.now(), timestamp: Date.now(),
isStreaming: false isStreaming: false,
}) });
await scrollToBottom() await scrollToBottom();
// 添加AI消息容器(流式接收) // 添加AI消息容器(流式接收)
const aiMessageIndex = messages.value.length const aiMessageIndex = messages.value.length;
messages.value.push({ messages.value.push({
content: '', content: "",
isUser: false, isUser: false,
agentId: selectedAgent.value, agentId: selectedAgent.value,
timestamp: Date.now(), timestamp: Date.now(),
isStreaming: true, isStreaming: true,
hasError: false, hasError: false,
originalMessage: userMessage originalMessage: userMessage,
}) });
isLoading.value = true isLoading.value = true;
let hasReceivedFirstToken = false // 标记是否接收到第一个token let hasReceivedFirstToken = false; // 标记是否接收到第一个token
try { try {
// 使用自定义的流式处理 // 使用自定义的流式处理
const token = localStorage.getItem('token') const token = localStorage.getItem("token");
const abortController = new AbortController() const abortController = new AbortController();
// 调用后端的流式API // 调用后端的流式API
const response = await fetch( const response = await fetch(
`/api/v1/agent/chat-stream?agentId=${selectedAgent.value}`, `/api/v1/agent/chat-stream?agentId=${selectedAgent.value}`,
{ {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
'Authorization': `Bearer ${token || ''}` Authorization: `Bearer ${token || ""}`,
}, },
body: JSON.stringify({ message: userMessage }), body: JSON.stringify({ message: userMessage }),
signal: abortController.signal signal: abortController.signal,
} }
) );
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`) throw new Error(`HTTP error! status: ${response.status}`);
} }
const reader = response.body?.getReader() const reader = response.body?.getReader();
if (!reader) { if (!reader) {
throw new Error('无法读取响应体') throw new Error("无法读取响应体");
} }
const accumulatedContentRef = { value: '' } const accumulatedContentRef = { value: "" };
const hasFinalAnswerRef = { value: false } const hasFinalAnswerRef = { value: false };
const currentEventRef = { value: '' } const currentEventRef = { value: "" };
const decoder = new TextDecoder() const decoder = new TextDecoder();
let buffer = '' let buffer = "";
let isStreamComplete = false // 标记流是否已完成 let isStreamComplete = false; // 标记流是否已完成
const STREAM_TIMEOUT = 60000 // 60秒无流式消息则为超时 const STREAM_TIMEOUT = 60000; // 60秒无流式消息则为超时
// 设置超时检查 // 设置超时检查
const resetStreamTimeout = () => { const resetStreamTimeout = () => {
clearStreamTimeout() clearStreamTimeout();
streamTimeoutTimer = setTimeout(() => { streamTimeoutTimer = setTimeout(() => {
if (!isStreamComplete) { if (!isStreamComplete) {
isStreamComplete = true isStreamComplete = true;
reader.cancel() reader.cancel();
messages.value[aiMessageIndex].isStreaming = false messages.value[aiMessageIndex].isStreaming = false;
isLoading.value = false isLoading.value = false;
// 提示用户是否要重试 // 提示用户是否要重试
ElMessage.warning('流式输出超时,您可以点击重试按钮重新发送消息') ElMessage.warning("流式输出超时,您可以点击重试按钮重新发送消息");
// 显示重试按钮 // 显示重试按钮
messages.value[aiMessageIndex].content = '[错误] 流式输出超时,请重试' messages.value[aiMessageIndex].content =
messages.value[aiMessageIndex].hasError = true "[错误] 流式输出超时,请重试";
messages.value[aiMessageIndex].hasError = true;
} }
}, STREAM_TIMEOUT); }, STREAM_TIMEOUT);
} };
resetStreamTimeout() resetStreamTimeout();
while (true) { while (true) {
if (isStreamComplete) break // 如果已收到complete事件,停止读取 if (isStreamComplete) break; // 如果已收到complete事件,停止读取
const { done, value } = await reader.read() const { done, value } = await reader.read();
if (done) break if (done) break;
buffer += decoder.decode(value, { stream: true }) buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n') const lines = buffer.split("\n");
buffer = lines.pop() || '' buffer = lines.pop() || "";
for (const line of lines) { for (const line of lines) {
const isComplete = await processSSELine(line, accumulatedContentRef, hasFinalAnswerRef, currentEventRef, aiMessageIndex, resetStreamTimeout) const isComplete = await processSSELine(
line,
accumulatedContentRef,
hasFinalAnswerRef,
currentEventRef,
aiMessageIndex,
resetStreamTimeout
);
if (isComplete) { if (isComplete) {
isStreamComplete = true isStreamComplete = true;
} }
// 在接收到第一个token时,立即隐藏Skeleton加载动画 // 在接收到第一个token时,立即隐藏Skeleton加载动画
if (!hasReceivedFirstToken && (line.includes('"type":"token"') || currentEventRef.value === 'token')) { if (
isLoading.value = false !hasReceivedFirstToken &&
hasReceivedFirstToken = true (line.includes('"type":"token"') || currentEventRef.value === "token")
) {
isLoading.value = false;
hasReceivedFirstToken = true;
} }
} }
} }
// 处理残余的数据 // 处理残余的数据
if (buffer && !isStreamComplete) { if (buffer && !isStreamComplete) {
const lines = buffer.split('\n') const lines = buffer.split("\n");
for (const line of lines) { for (const line of lines) {
const isComplete = await processSSELine(line, accumulatedContentRef, hasFinalAnswerRef, currentEventRef, aiMessageIndex, resetStreamTimeout) const isComplete = await processSSELine(
line,
accumulatedContentRef,
hasFinalAnswerRef,
currentEventRef,
aiMessageIndex,
resetStreamTimeout
);
if (isComplete) { if (isComplete) {
isStreamComplete = true isStreamComplete = true;
} }
// 在接收到第一个token时,立即隐藏Skeleton加载动画 // 在接收到第一个token时,立即隐藏Skeleton加载动画
if (!hasReceivedFirstToken && (line.includes('"type":"token"') || currentEventRef.value === 'token')) { if (
isLoading.value = false !hasReceivedFirstToken &&
hasReceivedFirstToken = true (line.includes('"type":"token"') || currentEventRef.value === "token")
) {
isLoading.value = false;
hasReceivedFirstToken = true;
} }
} }
} }
...@@ -722,58 +829,62 @@ const sendMessage = async () => { ...@@ -722,58 +829,62 @@ const sendMessage = async () => {
// 修复:仅有在没有最终答案且没有错误的情况下才更新消息内容 // 修复:仅有在没有最终答案且没有错误的情况下才更新消息内容
// 如果已经有最终答案或错误信息,就不需要再更新消息内容了,避免重复显示 // 如果已经有最终答案或错误信息,就不需要再更新消息内容了,避免重复显示
if (!hasFinalAnswerRef.value && !messages.value[aiMessageIndex].hasError) { if (!hasFinalAnswerRef.value && !messages.value[aiMessageIndex].hasError) {
messages.value[aiMessageIndex].content = accumulatedContentRef.value messages.value[aiMessageIndex].content = accumulatedContentRef.value;
} }
// 确保最终状态正确 // 确保最终状态正确
messages.value[aiMessageIndex].isStreaming = false messages.value[aiMessageIndex].isStreaming = false;
// 设置isLoading为false,结束加载状态 // 设置isLoading为false,结束加载状态
isLoading.value = false isLoading.value = false;
// 清除超时计时器 // 清除超时计时器
clearStreamTimeout() clearStreamTimeout();
// 确保滚动到底部 // 确保滚动到底部
await scrollToBottom() await scrollToBottom();
} catch (error: any) { } catch (error: any) {
// 清除超时计时器 // 清除超时计时器
clearStreamTimeout() clearStreamTimeout();
// 判断是否是网络错误 // 判断是否是网络错误
let errorMessage = '[错误] ' let errorMessage = "[错误] ";
// 检查是否是API密钥错误的特殊提示 // 检查是否是API密钥错误的特殊提示
if (error.message && (error.message.includes('请配置API密钥') || error.message.includes('[错误] 请配置API密钥'))) { if (
errorMessage = '[错误] 请配置API密钥' error.message &&
(error.message.includes("请配置API密钥") ||
error.message.includes("[错误] 请配置API密钥"))
) {
errorMessage = "[错误] 请配置API密钥";
} else if (error instanceof TypeError) { } else if (error instanceof TypeError) {
errorMessage += '网络获取失败,请检查你的网络连接' errorMessage += "网络获取失败,请检查你的网络连接";
} else if (error.name === 'AbortError') { } else if (error.name === "AbortError") {
errorMessage += '请求已取消' errorMessage += "请求已取消";
} else if (error.message && error.message.includes('处理超时')) { } else if (error.message && error.message.includes("处理超时")) {
errorMessage = '[错误] 服务器处理超时,请稍后重试' errorMessage = "[错误] 服务器处理超时,请稍后重试";
} else if (error.message) { } else if (error.message) {
errorMessage += error.message errorMessage += error.message;
} else { } else {
errorMessage += '一个未知错误发生' errorMessage += "一个未知错误发生";
} }
messages.value[aiMessageIndex].content = errorMessage messages.value[aiMessageIndex].content = errorMessage;
messages.value[aiMessageIndex].isStreaming = false messages.value[aiMessageIndex].isStreaming = false;
messages.value[aiMessageIndex].hasError = true messages.value[aiMessageIndex].hasError = true;
isLoading.value = false isLoading.value = false;
} }
} };
onMounted(async () => { onMounted(async () => {
await loadAgents() await loadAgents();
// 等待下一个tick确保agents加载完成后再加载历史消息 // 等待下一个tick确保agents加载完成后再加载历史消息
await nextTick() await nextTick();
loadHistoryMessages() loadHistoryMessages();
}) });
// 暴露方法给父组件使用 // 暴露方法给父组件使用
defineExpose({ defineExpose({
loadHistoryByAgentId loadHistoryByAgentId,
}) });
</script> </script>
<style scoped> <style scoped>
......
<template>
<hi-page-template
ref="templateRef"
:json="json"
:open-intl="false"
></hi-page-template>
<div class="button-wrap">
<a-button type="primary" @click="submit">提交</a-button>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import HiPageTemplate from "pangea-ui/hi-page-template";
const templateRef = ref();
const submit = () => {
templateRef.value?.ctx.validate(1, (res, data) => {
console.log(res, data);
});
};
const json = {
pages: [
{
key: 0,
type: "default",
name: "默认页",
code: "",
display: "",
props: {
margin: "16px",
padding: "12px",
backgroundColor: "white",
display: {},
},
bindProps: {},
coms: [
{
key: 1,
type: "node",
name: "表单容器",
code: "HiFormContainer",
display: "",
props: {
status: "default",
backgroundColor: "transparent",
layout: "horizontal",
size: "medium",
labelAlign: "right",
display: {},
borderRadius: {},
boxShadow: {},
loop: {
data: [],
},
},
bindProps: {},
coms: [
{
key: 1766473421208,
name: "输入框",
code: "HiInput",
props: {
title: "输入框",
status: "default",
placeholder: "请输入",
name: "INPUT_6CP8HIBK",
},
bindProps: {},
coms: [],
},
{
key: 1766476676439,
name: "日期",
code: "HiDatePicker",
props: {
title: "日期",
type: "date",
format: "YYYY-MM-DD",
status: "default",
name: "DATE_PA9TUPQQ",
},
bindProps: {},
},
],
},
],
},
],
params: [],
apis: [],
funcs: [],
pageTemplate: {},
};
</script>
<style scoped>
.button-wrap {
display: flex;
justify-content: center;
}
</style>
<template> <template>
<div class="work-area"> <div class="work-area">
<el-tabs v-model="activeTab" class="work-tabs"> <el-tabs v-model="activeTab" class="work-tabs">
<el-tab-pane label="表单" name="form">
<form-render ref="formRender" />
</el-tab-pane>
<el-tab-pane label="📋 时间轴" name="timeline"> <el-tab-pane label="📋 时间轴" name="timeline">
<timeline-container ref="timelineContainerRef" /> <timeline-container ref="timelineContainerRef" />
</el-tab-pane> </el-tab-pane>
...@@ -12,108 +15,121 @@ ...@@ -12,108 +15,121 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue' import { ref, onMounted, onUnmounted } from "vue";
import TimelineContainer from './TimelineContainer.vue' import FormRender from "./FormRender.vue";
import WebpageBrowser from './WebpageBrowser.vue' import TimelineContainer from "./TimelineContainer.vue";
import { TimelineService } from '../services/TimelineService' import WebpageBrowser from "./WebpageBrowser.vue";
import { TimelineService } from "../services/TimelineService";
const activeTab = ref('timeline')
const timelineContainerRef = ref<InstanceType<typeof TimelineContainer> | null>(null) const activeTab = ref("form");
const webBrowser = ref() const formRender = ref();
let timelineService: TimelineService | null = null const timelineContainerRef = ref<InstanceType<typeof TimelineContainer> | null>(
null
);
const webBrowser = ref();
let timelineService: TimelineService | null = null;
// 添加事件到时间轴 // 添加事件到时间轴
const addEvent = (event: any): void => { const addEvent = (event: any): void => {
timelineContainerRef.value?.addEvent(event) timelineContainerRef.value?.addEvent(event);
} };
// 初始化Timeline服务 // 初始化Timeline服务
const initTimelineService = () => { const initTimelineService = () => {
if (timelineContainerRef.value) { if (timelineContainerRef.value) {
timelineService = new TimelineService((event: any) => { timelineService = new TimelineService((event: any) => {
addEvent(event) addEvent(event);
}) });
timelineService.connectSSE() timelineService.connectSSE();
} }
} };
// 清除时间轴 // 清除时间轴
const clearTimeline = (): void => { const clearTimeline = (): void => {
timelineContainerRef.value?.clearTimeline() timelineContainerRef.value?.clearTimeline();
} };
// 定义embed事件的详细信息类型 // 定义embed事件的详细信息类型
interface EmbedEventDetail { interface EmbedEventDetail {
url: string url: string;
type: string type: string;
title: string title: string;
htmlContent?: string htmlContent?: string;
} }
// 监听embed事件 // 监听embed事件
const handleEmbedEvent = (e: Event) => { const handleEmbedEvent = (e: Event) => {
const customEvent = e as CustomEvent<EmbedEventDetail> const customEvent = e as CustomEvent<EmbedEventDetail>;
const { url, type, title, htmlContent } = customEvent.detail const { url, type, title, htmlContent } = customEvent.detail;
// 验证URL有效性 // 验证URL有效性
if (!url || typeof url !== 'string' || url.trim() === '') { if (!url || typeof url !== "string" || url.trim() === "") {
console.error('[WorkArea] embed事件URL验证失败:', { console.error("[WorkArea] embed事件URL验证失败:", {
url: url, url: url,
type: typeof url, type: typeof url,
isEmpty: url?.trim() === '', isEmpty: url?.trim() === "",
detail: customEvent.detail detail: customEvent.detail,
}) });
return return;
} }
// 自动切换到浏览器标签页 // 自动切换到浏览器标签页
activeTab.value = 'browser' activeTab.value = "browser";
// 调用WebpageBrowser的导航方法,传递完整信息 // 调用WebpageBrowser的导航方法,传递完整信息
if (webBrowser.value && typeof webBrowser.value.navigateToUrl === 'function') { if (
webBrowser.value &&
typeof webBrowser.value.navigateToUrl === "function"
) {
webBrowser.value.navigateToUrl(url, { webBrowser.value.navigateToUrl(url, {
htmlContent: htmlContent, htmlContent: htmlContent,
embedType: type, embedType: type,
embedTitle: title embedTitle: title,
}) });
} else { } else {
console.error('[WorkArea] webBrowser引用无效或navigateToUrl方法不存在', { console.error("[WorkArea] webBrowser引用无效或navigateToUrl方法不存在", {
hasWebBrowser: !!webBrowser.value, hasWebBrowser: !!webBrowser.value,
hasFn: webBrowser.value ? typeof webBrowser.value.navigateToUrl : 'undefined' hasFn: webBrowser.value
}) ? typeof webBrowser.value.navigateToUrl
: "undefined",
});
} }
} };
onMounted(() => { onMounted(() => {
// 监听embed事件 // 监听embed事件
window.addEventListener('embed-event', handleEmbedEvent as EventListener) window.addEventListener("embed-event", handleEmbedEvent as EventListener);
// 初始化Timeline服务 // 初始化Timeline服务
initTimelineService() initTimelineService();
}) });
onUnmounted(() => { onUnmounted(() => {
// 移除事件监听 // 移除事件监听
window.removeEventListener('embed-event', handleEmbedEvent as EventListener) window.removeEventListener("embed-event", handleEmbedEvent as EventListener);
// 清理Timeline服务 // 清理Timeline服务
if (timelineService) { if (timelineService) {
timelineService.cleanup() timelineService.cleanup();
} }
})// 暴露方法供父组件调用 }); // 暴露方法供父组件调用
defineExpose({ defineExpose({
formRender,
timelineContainerRef, timelineContainerRef,
webBrowser, webBrowser,
activeTab, activeTab,
// 提供切换tab的方法 // 提供切换tab的方法
switchToForm: () => {
activeTab.value = "form";
},
switchToTimeline: () => { switchToTimeline: () => {
activeTab.value = 'timeline' activeTab.value = "timeline";
}, },
switchToBrowser: () => { switchToBrowser: () => {
activeTab.value = 'browser' activeTab.value = "browser";
}, },
// 提供时间轴操作方法 // 提供时间轴操作方法
addEvent, addEvent,
clearTimeline clearTimeline,
}) });
</script> </script>
<style scoped> <style scoped>
......
...@@ -4,6 +4,9 @@ import router from './router' ...@@ -4,6 +4,9 @@ import router from './router'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import ElementPlus from 'element-plus' import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css' import 'element-plus/dist/index.css'
import ArcoVue from "@arco-design/web-vue";
import ArcoVueIcon from "@arco-design/web-vue/es/icon";
import '@arco-design/web-vue/dist/arco.less';
import 'highlight.js/styles/atom-one-dark.css' import 'highlight.js/styles/atom-one-dark.css'
import './styles/variables.css' import './styles/variables.css'
import './styles/global.css' import './styles/global.css'
...@@ -17,6 +20,8 @@ const app = createApp(App) ...@@ -17,6 +20,8 @@ const app = createApp(App)
app.use(createPinia()) app.use(createPinia())
app.use(router) app.use(router)
app.use(ElementPlus) app.use(ElementPlus)
app.use(ArcoVue)
app.use(ArcoVueIcon)
// 全局注册所有Element Plus图标 // 全局注册所有Element Plus图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) { for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment