feat: 加载历史记录

This commit is contained in:
2026-04-11 17:23:50 +08:00
parent 8c2ea4488b
commit 2ab6dd1050
2 changed files with 184 additions and 117 deletions

View File

@@ -1,17 +1,5 @@
<template>
<div class="chat-page">
<!-- 服务状态栏 -->
<div class="status-bar">
<div class="status-indicator">
<span class="dot" :class="serviceStatus" />
<span class="status-text">{{ statusText }}</span>
</div>
<div class="status-actions">
<el-button v-if="!isRunning" size="small" type="primary" :loading="isStarting" @click="startService"> 启动服务 </el-button>
<el-button v-else size="small" type="danger" plain @click="stopService"> 停止服务 </el-button>
</div>
</div>
<!-- 消息列表 -->
<div ref="messagesRef" class="messages">
<div v-if="messages.length === 0" class="empty-hint">
@@ -32,23 +20,23 @@
type="textarea"
:autosize="{ minRows: 2, maxRows: 5 }"
placeholder="输入消息Ctrl+Enter 发送"
:disabled="!isRunning || isSending"
:disabled="isSending"
resize="none"
@keydown.ctrl.enter.prevent="send"
/>
<el-button type="primary" :disabled="!isRunning || isSending || !inputText.trim()" :loading="isSending" @click="send"> 发送 </el-button>
<el-button type="primary" :disabled="isSending || !inputText.trim()" :loading="isSending" @click="send"> 发送 </el-button>
</div>
</div>
</template>
<script setup>
import { ref, computed, onUnmounted, nextTick } from 'vue';
import { ref, computed, onUnmounted, nextTick, watch } from 'vue';
import { useRoute } from 'vue-router';
import { ElMessage } from 'element-plus';
import { ChatDotRound } from '@element-plus/icons-vue';
import { createEventSource } from '@/http/api.js';
import { createEventSource, listMessagesAction } from '@/http/api.js';
const isRunning = ref(false);
const isStarting = ref(false);
const route = useRoute();
const isSending = ref(false);
const inputText = ref('');
const messages = ref([]);
@@ -57,15 +45,61 @@ const currentSessionId = ref(null);
const assistantMessageIds = new Set();
let eventSource = null;
const serviceStatus = computed(() => {
if (isStarting.value) return 'starting';
return isRunning.value ? 'running' : 'stopped';
});
// 从路由参数中获取 sessionId
const routeSessionId = computed(() => route.query.sessionId);
const statusText = computed(() => {
if (isStarting.value) return '正在启动...';
return isRunning.value ? '服务运行中' : '服务未启动';
});
// 加载历史消息
async function loadHistoryMessages(sessionId) {
if (!sessionId) return;
try {
const messagesData = await listMessagesAction(sessionId);
// 清空当前消息
messages.value = [];
assistantMessageIds.clear();
// 处理历史消息
messagesData.forEach((item) => {
const { info, parts } = item;
if (!info || !parts) return;
// 提取文本内容
const text = parts
.filter((part) => part.type === 'text')
.map((part) => part.text)
.join('');
if (text) {
messages.value.push({
id: info.id,
role: info.role,
text: text,
});
// 记录 assistant 消息 ID
if (info.role === 'assistant') {
assistantMessageIds.add(info.id);
}
}
});
scrollToBottom();
} catch (err) {
console.error('加载历史消息失败:', err);
ElMessage.error(`加载历史消息失败: ${err.message}`);
}
}
// 监听路由参数变化,加载对应会话
watch(
routeSessionId,
(newSessionId) => {
if (newSessionId) {
currentSessionId.value = newSessionId;
loadHistoryMessages(newSessionId);
}
},
{ immediate: true }
);
function scrollToBottom() {
nextTick(() => {
@@ -128,35 +162,6 @@ function connectSSE() {
};
}
async function startService() {
isStarting.value = true;
try {
const info = await window.opencode.start();
isRunning.value = info.running;
// 更新 baseUrl 供 http 层使用
if (info.url) window.__opencodeBaseUrl = info.url;
connectSSE();
ElMessage.success('服务已启动');
} catch (err) {
ElMessage.error(`启动失败: ${err.message}`);
} finally {
isStarting.value = false;
}
}
async function stopService() {
await window.opencode.stop();
isRunning.value = false;
currentSessionId.value = null;
messages.value = [];
assistantMessageIds.clear();
if (eventSource) {
eventSource.close();
eventSource = null;
}
ElMessage.info('服务已停止');
}
async function send() {
const text = inputText.value.trim();
if (!text || isSending.value) return;
@@ -186,18 +191,6 @@ async function send() {
}
}
// 初始化时同步服务状态
window.opencode
?.info()
.then((info) => {
isRunning.value = info.running;
if (info.running) {
if (info.url) window.__opencodeBaseUrl = info.url;
connectSSE();
}
})
.catch(() => {});
onUnmounted(() => {
if (eventSource) eventSource.close();
});
@@ -212,52 +205,6 @@ onUnmounted(() => {
gap: 12px;
}
.status-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 16px;
background: #fff;
border-radius: 10px;
border: 1px solid #e4e7ed;
}
.status-indicator {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #606266;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.dot.stopped {
background: #c0c4cc;
}
.dot.starting {
background: #e6a23c;
animation: pulse 1s infinite;
}
.dot.running {
background: #67c23a;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.3;
}
}
.messages {
flex: 1;
min-height: 0;