Files
Web_BLS_ProjectConsole/src/frontend/components/ProjectSelector.vue
XuJiacheng 282f7268ed feat: 重构项目心跳数据结构并实现项目列表API
- 新增统一项目列表Redis键和迁移工具
- 实现GET /api/projects端点获取项目列表
- 实现POST /api/projects/migrate端点支持数据迁移
- 更新前端ProjectSelector组件使用真实项目数据
- 扩展projectStore状态管理
- 更新相关文档和OpenSpec规范
- 添加测试用例验证新功能
2026-01-13 19:45:05 +08:00

333 lines
7.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="project-selector">
<!-- 组件标题 -->
<div class="selector-header">
<h2>项目选择</h2>
</div>
<!-- 加载/错误提示 -->
<div v-if="loading" class="state-text">
正在加载项目列表...
</div>
<div v-else-if="error" class="state-text state-error">
{{ error }}
</div>
<div v-else-if="noneConnected" class="state-text">
暂无连接
</div>
<!-- 项目列表 -->
<div class="project-list">
<div v-for="project in projects" :key="project.id" class="project-item"
:class="{ 'project-selected': selectedProjectId === project.id }" @click="selectProject(project)">
<!-- 项目状态指示器 -->
<div class="project-status" :class="`status-${project.status}`" />
<!-- 项目信息 -->
<div class="project-info">
<div class="project-name">
{{ project.name }}
</div>
<div class="project-description">
{{ project.apiBaseUrl || '未上报 API 地址' }}
</div>
<div class="project-meta">
<span class="project-status-text">
{{ getStatusText(project.status) }}
</span>
<span class="project-heartbeat">
{{ formatLastActiveAt(project.lastActiveAt) }}
</span>
</div>
</div>
<!-- 选择指示器 -->
<div v-if="selectedProjectId === project.id" class="project-select-indicator">
<span class="select-icon"></span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed, onMounted, onUnmounted, ref } from 'vue';
import axios from 'axios';
import { projectsList, setProjectsList } from '../store/projectStore.js';
defineProps({
selectedProjectId: {
type: String,
default: null,
},
});
const emit = defineEmits(['project-selected']);
const loading = ref(false);
const error = ref('');
const noneConnected = ref(false);
const initialized = ref(false);
let refreshTimer = null;
const projects = computed(() => projectsList.value || []);
function scheduleRefresh(ms) {
if (refreshTimer) clearInterval(refreshTimer);
refreshTimer = setInterval(fetchProjects, ms);
}
const fetchProjects = async () => {
if (!initialized.value) loading.value = true;
error.value = '';
try {
const resp = await axios.get('/api/projects');
if (resp.status === 200 && resp.data && resp.data.success) {
const list = resp.data.projects || [];
setProjectsList(Array.isArray(list) ? list : []);
if (Array.isArray(list) && list.length === 0) {
// Redis 就绪但没有任何心跳数据 → 显示“暂无连接”每2秒重试
noneConnected.value = true;
error.value = '';
scheduleRefresh(2000);
} else {
noneConnected.value = false;
scheduleRefresh(5000);
}
} else if (resp.status === 503) {
// Redis 未就绪显示错误并每5秒重试
setProjectsList([]);
noneConnected.value = false;
error.value = resp.data?.message || 'Redis 未就绪';
scheduleRefresh(5000);
} else {
// 其他非 200 情况当作错误
setProjectsList([]);
noneConnected.value = false;
error.value = resp.data?.message || `获取项目列表失败 (${resp.status})`;
scheduleRefresh(5000);
}
} catch (err) {
console.error('Failed to fetch projects:', err);
error.value = err?.response?.data?.message || err?.message || '获取项目列表失败';
setProjectsList([]);
noneConnected.value = false;
scheduleRefresh(5000);
} finally {
loading.value = false;
initialized.value = true;
}
};
const selectProject = (project) => {
emit('project-selected', project);
};
const getStatusText = (status) => {
const statusMap = {
online: '在线',
offline: '离线',
unknown: '未知',
};
return statusMap[status] || status;
};
const formatLastActiveAt = (lastActiveAt) => {
if (!lastActiveAt) return '无心跳时间';
const ts = typeof lastActiveAt === 'number' ? lastActiveAt : Number(lastActiveAt);
if (!Number.isFinite(ts)) return '无心跳时间';
const date = new Date(ts);
return `心跳: ${date.toLocaleString('zh-CN')}`;
};
onMounted(() => {
fetchProjects();
refreshTimer = setInterval(fetchProjects, 5000);
});
onUnmounted(() => {
if (refreshTimer) clearInterval(refreshTimer);
});
</script>
<style scoped>
.project-selector {
display: flex;
flex-direction: column;
height: 100%;
padding: 1rem;
background-color: #252526;
box-sizing: border-box;
}
.selector-header {
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid #3e3e42;
}
.selector-header h2 {
font-size: 1.2rem;
font-weight: 600;
color: #e0e0e0;
margin: 0;
}
/* 项目列表 */
.project-list {
flex: 1;
overflow-y: auto;
max-height: calc(100vh - 150px);
}
.state-text {
padding: 0.8rem;
color: #b0b0b0;
}
.state-error {
color: #ea4335;
}
.project-item {
display: flex;
align-items: center;
padding: 0.8rem;
margin-bottom: 0.5rem;
background-color: #333333;
border: 1px solid #444444;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
position: relative;
}
.project-item:hover {
background-color: #3a3a3a;
border-color: #4285f4;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.project-item.project-selected {
background-color: #2c3e50;
border-color: #4285f4;
box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.3);
}
/* 项目状态指示器 */
.project-status {
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 0.8rem;
flex-shrink: 0;
}
.status-online {
background-color: #34a853;
}
.status-offline {
background-color: #fbbc05;
}
.status-unknown {
background-color: #ea4335;
}
/* 项目信息 */
.project-info {
flex: 1;
min-width: 0;
}
.project-name {
font-size: 0.95rem;
font-weight: 600;
color: #e0e0e0;
margin-bottom: 0.2rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.project-description {
font-size: 0.8rem;
color: #b0b0b0;
margin-bottom: 0.3rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.project-meta {
display: flex;
gap: 0.8rem;
align-items: center;
}
.project-heartbeat {
font-size: 0.75rem;
color: #999;
white-space: nowrap;
}
.project-type {
font-size: 0.75rem;
padding: 0.15rem 0.5rem;
border-radius: 10px;
font-weight: 500;
}
.type-backend {
background-color: #1e3a2a;
color: #4ade80;
}
.type-frontend {
background-color: #2d1b4e;
color: #c084fc;
}
.project-status-text {
font-size: 0.75rem;
color: #999;
}
/* 选择指示器 */
.project-select-indicator {
width: 24px;
height: 24px;
background-color: #4285f4;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 0.8rem;
margin-left: 0.5rem;
flex-shrink: 0;
}
/* 滚动条样式 */
.project-list::-webkit-scrollbar {
width: 6px;
}
.project-list::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.project-list::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.project-list::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
</style>