218 lines
7.9 KiB
Vue
218 lines
7.9 KiB
Vue
<template>
|
||
<div class="flex flex-col h-screen w-screen overflow-hidden">
|
||
<div class="flex flex-1 overflow-hidden">
|
||
<!-- 侧边栏 -->
|
||
<aside :class="['flex flex-col bg-[#EEEFF2] border-r border-gray-200 transition-all duration-300 no-drag', appStore.collapsed ? 'w-16' : 'w-[244px]']">
|
||
<!-- 自定义菜单 -->
|
||
<nav class="flex-1 px-3 py-4 space-y-1 overflow-y-auto">
|
||
<div
|
||
v-for="(item, index) in menus"
|
||
:key="index"
|
||
:class="[
|
||
'flex items-center justify-between cursor-pointer transition-all duration-200 ease-out group relative overflow-hidden',
|
||
appStore.collapsed ? 'justify-center px-0 py-3 mx-auto w-11 rounded-xl' : 'w-[216px] h-[37px] rounded-[10px] pl-3 pr-[146px] py-0 mx-auto',
|
||
$route.path === item.index ? 'bg-[#DEE0E4] text-gray-900' : 'text-gray-500 hover:bg-gray-50 hover:text-gray-700',
|
||
]"
|
||
@click="router.push(item.index)"
|
||
>
|
||
<!-- 图标 -->
|
||
<div
|
||
:class="[
|
||
'flex items-center justify-center shrink-0 transition-transform duration-200',
|
||
appStore.collapsed ? 'w-6 h-6' : 'w-5 h-5',
|
||
$route.path === item.index ? 'scale-110' : 'group-hover:scale-105',
|
||
]"
|
||
>
|
||
<LucideIcon :name="item.icon" :size="appStore.collapsed ? 20 : 18" :class="['transition-colors duration-200']" />
|
||
</div>
|
||
|
||
<!-- 文字 -->
|
||
<span v-show="!appStore.collapsed" class="ml-3 text-sm font-medium whitespace-nowrap transition-all duration-200">
|
||
{{ item.name }}
|
||
</span>
|
||
</div>
|
||
<el-divider></el-divider>
|
||
<div class="w-54 h-8 text-sm mx-3 py-4 flex items-center" style="color: #8a9097">历史记录</div>
|
||
<div
|
||
v-for="(item, index) in historyItems"
|
||
:key="index"
|
||
class="flex items-center w-[216px] h-[37px] justify-between rounded-[10px] pl-3 pr-3 mx-auto text-gray-500 hover:bg-gray-50 hover:text-gray-700 cursor-pointer transition-all duration-200"
|
||
@click="onHistoryClick(item)"
|
||
:title="item.name"
|
||
>
|
||
<span class="text-sm font-medium whitespace-nowrap truncate flex-1">{{ item.name }}</span>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- 服务状态栏 -->
|
||
<div class="flex items-center justify-between px-3 py-2 border-t border-gray-200">
|
||
<div class="flex items-center gap-2">
|
||
<span class="w-2 h-2 rounded-full" :class="isServiceRunning ? 'bg-green-500' : 'bg-gray-400'" />
|
||
<span class="text-xs text-gray-600">
|
||
{{ isServiceRunning ? '服务运行中' : '服务未启动' }}
|
||
</span>
|
||
</div>
|
||
<el-button v-if="!isServiceRunning" size="small" type="primary" @click="startService"> 启动 </el-button>
|
||
<el-button v-else size="small" type="danger" plain @click="stopService"> 停止 </el-button>
|
||
</div>
|
||
|
||
<!-- 用户区域 -->
|
||
<div class="flex items-center justify-between p-3 border-t border-gray-200">
|
||
<div class="flex items-center">
|
||
<el-avatar :size="32" />
|
||
<div v-show="!appStore.collapsed" class="ml-3">
|
||
<div class="text-sm font-medium">123</div>
|
||
<div class="text-xs text-gray-500">12321</div>
|
||
</div>
|
||
</div>
|
||
<LucideIcon v-show="!appStore.collapsed" name="bolt" color="#808080" size="18"></LucideIcon>
|
||
</div>
|
||
|
||
<!-- <!– 折叠按钮 –>-->
|
||
<!-- <div class="p-3 border-t border-gray-200">-->
|
||
<!-- <el-button :icon="appStore.collapsed ? Expand : Fold" circle size="small" @click="appStore.toggleSidebar" />-->
|
||
<!-- </div>-->
|
||
</aside>
|
||
|
||
<!-- 主内容区 -->
|
||
<div class="flex flex-col flex-1 overflow-hidden">
|
||
<!-- 页面内容 -->
|
||
<main class="flex-1 overflow-hidden p-6">
|
||
<div class="h-full overflow-auto">
|
||
<router-view />
|
||
</div>
|
||
</main>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted, onUnmounted } from 'vue';
|
||
import { useRoute } from 'vue-router';
|
||
import { useAppStore } from '@/stores/app';
|
||
import { House, Monitor, Expand, Fold, ChatDotRound, Search, Collection, Clock } from '@element-plus/icons-vue';
|
||
import router from '@/router';
|
||
import axios from 'axios';
|
||
|
||
const route = useRoute();
|
||
const appStore = useAppStore();
|
||
|
||
const menus = ref([
|
||
{ name: '新对话', index: '/', icon: 'plus' },
|
||
{ name: '知识空间', index: '/knowledge', icon: 'book' },
|
||
{ name: 'opencode对话', index: '/chat', icon: 'bot' },
|
||
{ name: '发现设备', index: '/bonjour', icon: 'server' },
|
||
]);
|
||
|
||
const historyItems = ref([]);
|
||
const isServiceRunning = ref(false);
|
||
let checkInterval = null;
|
||
|
||
// 查询历史会话列表
|
||
async function loadHistorySessions() {
|
||
console.log('[loadHistorySessions] 开始加载历史会话...');
|
||
try {
|
||
const baseUrl = window.__opencodeBaseUrl || 'http://127.0.0.1:4096';
|
||
console.log('[loadHistorySessions] baseUrl:', baseUrl);
|
||
const response = await axios.get(`${baseUrl}/session`);
|
||
console.log('[loadHistorySessions] 响应数据:', response.data);
|
||
const sessions = response.data || [];
|
||
console.log('[loadHistorySessions] 会话数量:', sessions.length);
|
||
// 将会话列表转换为历史记录格式
|
||
historyItems.value = sessions.map((session) => ({
|
||
id: session.id,
|
||
name: session.title || `会话 ${session.slug || session.id.slice(0, 8)}...`,
|
||
slug: session.slug,
|
||
created: session.time?.created,
|
||
updated: session.time?.updated,
|
||
}));
|
||
console.log('[loadHistorySessions] 转换后的历史记录:', historyItems.value);
|
||
} catch (err) {
|
||
console.error('[loadHistorySessions] 加载历史会话失败:', err);
|
||
}
|
||
}
|
||
|
||
// 点击历史会话,跳转到对话页面并加载该会话
|
||
async function onHistoryClick(item) {
|
||
try {
|
||
const baseUrl = window.__opencodeBaseUrl || 'http://127.0.0.1:4096';
|
||
const detailUrl = `${baseUrl}/session/${item.id}/message`;
|
||
const response = await axios.get(detailUrl);
|
||
console.log('[onHistoryClick] 会话详情数据:', response.data);
|
||
} catch (err) {
|
||
console.error('[onHistoryClick] 获取会话详情失败:', err);
|
||
}
|
||
// 跳转到对话页面
|
||
router.push({
|
||
path: '/chat',
|
||
query: { sessionId: item.id },
|
||
});
|
||
}
|
||
|
||
// 启动服务
|
||
async function startService() {
|
||
try {
|
||
const info = await window.opencode.start();
|
||
isServiceRunning.value = info.running;
|
||
// 更新 baseUrl 供 http 层使用
|
||
if (info.url) window.__opencodeBaseUrl = info.url;
|
||
} catch (err) {
|
||
console.error('启动服务失败:', err);
|
||
}
|
||
}
|
||
|
||
// 停止服务
|
||
async function stopService() {
|
||
try {
|
||
await window.opencode.stop();
|
||
isServiceRunning.value = false;
|
||
historyItems.value = [];
|
||
} catch (err) {
|
||
console.error('停止服务失败:', err);
|
||
}
|
||
}
|
||
|
||
// 检查服务状态
|
||
async function checkServiceStatus() {
|
||
try {
|
||
const info = await window.opencode?.info();
|
||
const isRunning = info?.running || false;
|
||
|
||
// 更新服务状态
|
||
isServiceRunning.value = isRunning;
|
||
|
||
if (isRunning) {
|
||
// 服务运行中,更新 baseUrl 并加载历史记录
|
||
if (info?.url) {
|
||
window.__opencodeBaseUrl = info.url;
|
||
}
|
||
// 如果历史记录为空,则加载
|
||
if (historyItems.value.length === 0) {
|
||
loadHistorySessions();
|
||
}
|
||
} else {
|
||
// 服务未运行,清空历史记录
|
||
historyItems.value = [];
|
||
}
|
||
} catch (err) {
|
||
// 获取状态失败,视为服务断开
|
||
isServiceRunning.value = false;
|
||
historyItems.value = [];
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
// 初始检查
|
||
checkServiceStatus();
|
||
// 定期检查服务状态(每2秒)
|
||
checkInterval = setInterval(checkServiceStatus, 2000);
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
if (checkInterval) {
|
||
clearInterval(checkInterval);
|
||
}
|
||
});
|
||
</script>
|