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

952 lines
20 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="debug-area">
<!-- 调试区域标题和控制栏 -->
<div class="debug-header">
<h2>调试区域</h2>
<div class="debug-controls">
<button
class="export-btn"
@click="exportDebugInfo"
>
导出调试信息
</button>
<button
class="refresh-btn"
@click="refreshDebugInfo"
>
刷新数据
</button>
</div>
</div>
<!-- 筛选条件区域 -->
<div class="filters-container">
<div class="filter-group">
<div class="filter-item">
<label class="filter-label">项目名称</label>
<select
v-model="selectedProjectId"
class="filter-select"
>
<option value="all">
全部项目
</option>
<option
v-for="project in projects"
:key="project.id"
:value="project.id"
>
{{ project.name }}
</option>
</select>
</div>
<div class="filter-item">
<label class="filter-label">调试类型</label>
<select
v-model="selectedDebugType"
class="filter-select"
>
<option value="all">
全部类型
</option>
<option value="api">
API请求
</option>
<option value="database">
数据库操作
</option>
<option value="cache">
缓存操作
</option>
<option value="error">
错误信息
</option>
<option value="performance">
性能监控
</option>
</select>
</div>
</div>
<div class="filter-group">
<div class="filter-item">
<label class="filter-label">时间范围</label>
<div class="date-range">
<input
v-model="startTime"
type="datetime-local"
class="date-input"
>
<span class="date-separator"></span>
<input
v-model="endTime"
type="datetime-local"
class="date-input"
>
</div>
</div>
<div class="filter-actions">
<button
class="apply-filters-btn"
@click="applyFilters"
>
应用筛选
</button>
<button
class="reset-filters-btn"
@click="resetFilters"
>
重置筛选
</button>
</div>
</div>
</div>
<!-- 调试信息展示区域 -->
<div class="debug-content">
<!-- 调试信息列表 -->
<div class="debug-list">
<div
v-for="item in filteredDebugInfo"
:key="item.id"
class="debug-item"
:class="`debug-type-${item.type}`"
>
<div class="debug-item-header">
<div
class="debug-type-badge"
:class="`type-${item.type}`"
>
{{ getTypeText(item.type) }}
</div>
<div class="debug-timestamp">
{{ formatTimestamp(item.timestamp) }}
</div>
<div class="debug-project">
{{ getProjectName(item.projectId) }}
</div>
</div>
<div class="debug-item-content">
<div class="debug-content-main">
{{ item.content }}
</div>
<!-- 调试元数据 -->
<div
v-if="Object.keys(item.metadata).length > 0"
class="debug-metadata"
>
<h4>元数据</h4>
<div class="metadata-list">
<div
v-for="(value, key) in item.metadata"
:key="key"
class="metadata-item"
>
<span class="metadata-key">{{ key }}:</span>
<span class="metadata-value">
{{ JSON.stringify(value) }}
</span>
</div>
</div>
</div>
</div>
<!-- 展开/折叠按钮 -->
<div
class="expand-btn"
:class="{ expanded: expandedItems.includes(item.id) }"
@click="toggleExpand(item.id)"
>
<span class="expand-icon">
{{ expandedItems.includes(item.id) ? '▼' : '▶' }}
</span>
</div>
</div>
<!-- 空状态 -->
<div
v-if="filteredDebugInfo.length === 0"
class="empty-state"
>
<p>没有找到匹配的调试信息</p>
</div>
</div>
</div>
<!-- 分页控件 -->
<div
v-if="totalPages > 1"
class="pagination"
>
<button
class="page-btn"
:disabled="currentPage === 1"
@click="currentPage = 1"
>
首页
</button>
<button
class="page-btn"
:disabled="currentPage === 1"
@click="currentPage--"
>
上一页
</button>
<div class="page-info">
{{ currentPage }} / {{ totalPages }}
</div>
<button
class="page-btn"
:disabled="currentPage === totalPages"
@click="currentPage++"
>
下一页
</button>
<button
class="page-btn"
:disabled="currentPage === totalPages"
@click="currentPage = totalPages"
>
末页
</button>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
// 模拟项目数据
const projects = ref([
{ id: 'proj-1', name: '用户管理系统', type: 'backend' },
{ id: 'proj-2', name: '数据可视化平台', type: 'frontend' },
{ id: 'proj-3', name: '订单处理系统', type: 'backend' },
{ id: 'proj-4', name: '移动端应用', type: 'frontend' },
{ id: 'proj-5', name: 'API网关', type: 'backend' },
{ id: 'proj-6', name: '管理后台', type: 'frontend' },
]);
// 筛选条件
const selectedProjectId = ref('all');
const selectedDebugType = ref('all');
const startTime = ref('');
const endTime = ref('');
const expandedItems = ref([]);
// 分页设置
const currentPage = ref(1);
const pageSize = ref(10);
// 模拟调试信息数据
const debugInfo = ref([]);
// 初始化模拟数据
const initDebugData = () => {
const types = [ 'api', 'database', 'cache', 'error', 'performance' ];
const newDebugInfo = [];
// 生成50条模拟数据
for (let i = 0; i < 50; i++) {
const type = types[Math.floor(Math.random() * types.length)];
const projectId =
projects.value[Math.floor(Math.random() * projects.value.length)].id;
const timestamp = new Date(
Date.now() - Math.random() * 3600000 * 24,
).toISOString();
// 根据类型生成不同的内容
let content = '';
let metadata = {};
switch (type) {
case 'api':
content = `API请求: GET /api/users/${i + 1}`;
metadata = {
status: Math.random() > 0.1 ? 200 : 404,
responseTime: Math.floor(Math.random() * 500) + 50,
ip: `192.168.1.${Math.floor(Math.random() * 255)}`,
};
break;
case 'database':
content = `数据库操作: SELECT * FROM users WHERE id = ${i + 1}`;
metadata = {
queryTime: Math.floor(Math.random() * 100) + 10,
rowsAffected: Math.floor(Math.random() * 10) + 1,
table: 'users',
};
break;
case 'cache':
content = `缓存操作: SET user:${i + 1} = {id: ${i + 1}, name: 'User ${i + 1}'}`;
metadata = {
cacheKey: `user:${i + 1}`,
cacheType: Math.random() > 0.5 ? 'redis' : 'memcached',
ttl: 3600,
};
break;
case 'error':
content = '错误信息: Error: Cannot read property \'name\' of undefined';
metadata = {
errorType: 'TypeError',
stackTrace: [
'at Object.<anonymous> (/app/src/index.js:10:20)',
'at Module._compile (internal/modules/cjs/loader.js:1085:14)',
'at Object.Module._extensions..js (internal/modules/cjs/loader.js:1114:10)',
],
severity: 'high',
};
break;
case 'performance':
content = `性能监控: 页面加载时间: ${Math.floor(Math.random() * 2000) + 500}ms`;
metadata = {
metric: 'pageLoadTime',
value: Math.floor(Math.random() * 2000) + 500,
threshold: 1500,
status: Math.random() > 0.3 ? 'warning' : 'ok',
};
break;
}
newDebugInfo.push({
id: `debug-${Date.now()}-${i}`,
projectId,
timestamp,
type,
content,
metadata,
});
}
// 按时间降序排序
debugInfo.value = newDebugInfo.sort(
(a, b) => new Date(b.timestamp) - new Date(a.timestamp),
);
};
// 计算过滤后的调试信息
const filteredDebugInfo = computed(() => {
let result = [ ...debugInfo.value ];
// 按项目筛选
if (selectedProjectId.value !== 'all') {
result = result.filter(
(item) => item.projectId === selectedProjectId.value,
);
}
// 按调试类型筛选
if (selectedDebugType.value !== 'all') {
result = result.filter((item) => item.type === selectedDebugType.value);
}
// 按时间范围筛选
if (startTime.value) {
const startDate = new Date(startTime.value);
result = result.filter((item) => new Date(item.timestamp) >= startDate);
}
if (endTime.value) {
const endDate = new Date(endTime.value);
result = result.filter((item) => new Date(item.timestamp) <= endDate);
}
// 分页处理
const startIndex = (currentPage.value - 1) * pageSize.value;
const endIndex = startIndex + pageSize.value;
return result.slice(startIndex, endIndex);
});
// 计算总页数
const totalPages = computed(() => {
let result = [ ...debugInfo.value ];
// 应用相同的过滤条件来计算总条数
if (selectedProjectId.value !== 'all') {
result = result.filter(
(item) => item.projectId === selectedProjectId.value,
);
}
if (selectedDebugType.value !== 'all') {
result = result.filter((item) => item.type === selectedDebugType.value);
}
if (startTime.value) {
const startDate = new Date(startTime.value);
result = result.filter((item) => new Date(item.timestamp) >= startDate);
}
if (endTime.value) {
const endDate = new Date(endTime.value);
result = result.filter((item) => new Date(item.timestamp) <= endDate);
}
return Math.ceil(result.length / pageSize.value);
});
// 应用筛选
const applyFilters = () => {
// 重置到第一页
currentPage.value = 1;
console.log('应用筛选条件');
};
// 重置筛选
const resetFilters = () => {
selectedProjectId.value = 'all';
selectedDebugType.value = 'all';
startTime.value = '';
endTime.value = '';
currentPage.value = 1;
console.log('重置筛选条件');
};
// 刷新调试信息
const refreshDebugInfo = () => {
console.log('刷新调试信息');
initDebugData();
};
// 导出调试信息
const exportDebugInfo = () => {
console.log('导出调试信息');
// 这里可以实现导出功能例如导出为JSON或CSV文件
const dataToExport = {
exportTime: new Date().toISOString(),
filterConditions: {
selectedProjectId: selectedProjectId.value,
selectedDebugType: selectedDebugType.value,
startTime: startTime.value,
endTime: endTime.value,
},
debugInfo: filteredDebugInfo.value,
};
// 创建下载链接
const blob = new Blob([ JSON.stringify(dataToExport, null, 2) ], {
type: 'application/json',
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `debug-info-${new Date().toISOString().slice(0, 10)}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
// 切换展开/折叠状态
const toggleExpand = (id) => {
const index = expandedItems.value.indexOf(id);
if (index === -1) {
expandedItems.value.push(id);
} else {
expandedItems.value.splice(index, 1);
}
};
// 获取项目名称
const getProjectName = (projectId) => {
const project = projects.value.find((p) => p.id === projectId);
return project ? project.name : '未知项目';
};
// 获取调试类型文本
const getTypeText = (type) => {
const typeMap = {
api: 'API请求',
database: '数据库操作',
cache: '缓存操作',
error: '错误信息',
performance: '性能监控',
};
return typeMap[type] || type;
};
// 格式化时间戳
const formatTimestamp = (timestamp) => {
const date = new Date(timestamp);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
};
// 组件挂载时初始化数据
onMounted(() => {
initDebugData();
});
</script>
<style scoped>
.debug-area {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
display: flex;
flex-direction: column;
height: 100%;
min-height: 500px;
}
/* 调试区域标题和控制栏 */
.debug-header {
background-color: #f8f9fa;
padding: 1rem;
border-bottom: 1px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.debug-header h2 {
font-size: 1.2rem;
font-weight: 600;
margin: 0;
color: #333;
}
.debug-controls {
display: flex;
gap: 0.8rem;
}
.export-btn,
.refresh-btn {
padding: 0.5rem 1rem;
border: 1px solid #e0e0e0;
border-radius: 4px;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;
}
.export-btn {
background-color: #4285f4;
color: white;
border-color: #4285f4;
}
.export-btn:hover {
background-color: #3367d6;
border-color: #3367d6;
}
.refresh-btn {
background-color: white;
color: #666;
}
.refresh-btn:hover {
background-color: #f0f0f0;
color: #333;
}
/* 筛选条件区域 */
.filters-container {
padding: 1rem;
border-bottom: 1px solid #e0e0e0;
background-color: #fafafa;
flex-shrink: 0;
}
.filter-group {
display: flex;
gap: 1.5rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.filter-group:last-child {
margin-bottom: 0;
align-items: flex-end;
}
.filter-item {
display: flex;
flex-direction: column;
gap: 0.3rem;
min-width: 200px;
flex: 1;
max-width: 300px;
}
.filter-label {
font-size: 0.85rem;
font-weight: 600;
color: #666;
}
.filter-select,
.date-input {
padding: 0.5rem;
border: 1px solid #e0e0e0;
border-radius: 4px;
font-size: 0.9rem;
transition: border-color 0.2s;
}
.filter-select:focus,
.date-input:focus {
outline: none;
border-color: #4285f4;
box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.1);
}
.date-range {
display: flex;
align-items: center;
gap: 0.8rem;
}
.date-separator {
color: #999;
font-size: 0.9rem;
}
.filter-actions {
display: flex;
gap: 0.8rem;
}
.apply-filters-btn,
.reset-filters-btn {
padding: 0.5rem 1rem;
border: 1px solid #e0e0e0;
border-radius: 4px;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;
}
.apply-filters-btn {
background-color: #34a853;
color: white;
border-color: #34a853;
}
.apply-filters-btn:hover {
background-color: #2d8f47;
border-color: #2d8f47;
}
.reset-filters-btn {
background-color: white;
color: #666;
}
.reset-filters-btn:hover {
background-color: #f0f0f0;
color: #333;
}
/* 调试信息展示区域 */
.debug-content {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.debug-list {
flex: 1;
overflow-y: auto;
padding: 0 1rem;
}
.debug-item {
background-color: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
margin: 1rem 0;
padding: 1rem;
position: relative;
transition: all 0.2s;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.debug-item:hover {
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
border-color: #4285f4;
}
/* 调试类型样式 */
.debug-type-api {
border-left: 4px solid #4285f4;
}
.debug-type-database {
border-left: 4px solid #34a853;
}
.debug-type-cache {
border-left: 4px solid #fbbc05;
}
.debug-type-error {
border-left: 4px solid #ea4335;
}
.debug-type-performance {
border-left: 4px solid #673ab7;
}
.debug-item-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 0.8rem;
flex-wrap: wrap;
}
.debug-type-badge {
padding: 0.2rem 0.6rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
color: white;
text-transform: uppercase;
}
.type-api {
background-color: #4285f4;
}
.type-database {
background-color: #34a853;
}
.type-cache {
background-color: #fbbc05;
}
.type-error {
background-color: #ea4335;
}
.type-performance {
background-color: #673ab7;
}
.debug-timestamp {
font-size: 0.8rem;
color: #999;
flex-shrink: 0;
}
.debug-project {
font-size: 0.85rem;
color: #666;
font-weight: 500;
}
.debug-item-content {
margin-bottom: 0.5rem;
}
.debug-content-main {
font-size: 0.9rem;
color: #333;
line-height: 1.5;
margin-bottom: 0.8rem;
}
/* 元数据样式 */
.debug-metadata {
background-color: #f8f9fa;
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 0.8rem;
margin-top: 0.8rem;
}
.debug-metadata h4 {
font-size: 0.9rem;
font-weight: 600;
color: #666;
margin-bottom: 0.5rem;
}
.metadata-list {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.metadata-item {
display: flex;
gap: 0.5rem;
font-size: 0.85rem;
}
.metadata-key {
font-weight: 600;
color: #666;
min-width: 100px;
flex-shrink: 0;
}
.metadata-value {
color: #333;
word-break: break-all;
}
/* 展开/折叠按钮 */
.expand-btn {
position: absolute;
right: 1rem;
top: 1rem;
background: none;
border: none;
color: #999;
font-size: 1rem;
cursor: pointer;
padding: 0.2rem;
border-radius: 3px;
transition: all 0.2s;
}
.expand-btn:hover {
background-color: #f0f0f0;
color: #666;
}
.expand-btn.expanded {
color: #4285f4;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 2rem 0;
color: #999;
font-size: 0.9rem;
}
/* 分页控件 */
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 1rem;
border-top: 1px solid #e0e0e0;
background-color: #fafafa;
flex-shrink: 0;
}
.page-btn {
padding: 0.5rem 1rem;
border: 1px solid #e0e0e0;
border-radius: 4px;
background-color: white;
color: #666;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;
}
.page-btn:hover:not(:disabled) {
background-color: #f0f0f0;
color: #333;
border-color: #ccc;
}
.page-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.page-info {
font-size: 0.85rem;
color: #666;
}
/* 滚动条样式 */
.debug-list::-webkit-scrollbar {
width: 6px;
}
.debug-list::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.debug-list::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.debug-list::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 响应式设计 */
@media (max-width: 768px) {
.filter-group {
flex-direction: column;
gap: 1rem;
}
.filter-item {
max-width: 100%;
min-width: auto;
}
.filter-group:last-child {
align-items: stretch;
}
.filter-actions {
flex-direction: column;
gap: 0.5rem;
}
.debug-controls {
flex-direction: column;
gap: 0.5rem;
}
.export-btn,
.refresh-btn {
width: 100%;
}
.debug-item-header {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.expand-btn {
position: static;
align-self: flex-end;
margin-top: -0.5rem;
margin-bottom: 0.5rem;
}
.pagination {
flex-wrap: wrap;
gap: 0.5rem;
}
.page-btn {
padding: 0.4rem 0.8rem;
font-size: 0.8rem;
}
}
</style>