656 lines
27 KiB
HTML
656 lines
27 KiB
HTML
|
|
<!DOCTYPE html>
|
||
|
|
<html lang="zh-CN">
|
||
|
|
<head>
|
||
|
|
<meta charset="UTF-8">
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
|
|
<title>SDLC Agent Demo - 多智能体软件交付协同系统</title>
|
||
|
|
|
||
|
|
<!-- TailwindCSS -->
|
||
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
||
|
|
|
||
|
|
<!-- Vue 3 -->
|
||
|
|
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||
|
|
|
||
|
|
<!-- Highlight.js 代码高亮 - 使用浅色主题 -->
|
||
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
|
||
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||
|
|
|
||
|
|
<!-- Marked Markdown 解析 -->
|
||
|
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||
|
|
|
||
|
|
<style>
|
||
|
|
/* 自定义样式 */
|
||
|
|
.stage-card {
|
||
|
|
transition: all 0.3s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stage-card.active {
|
||
|
|
transform: scale(1.02);
|
||
|
|
box-shadow: 0 10px 25px rgba(59, 130, 246, 0.3);
|
||
|
|
}
|
||
|
|
|
||
|
|
.stage-card.completed {
|
||
|
|
border-color: #10B981;
|
||
|
|
background: linear-gradient(135deg, #ECFDF5 0%, #FFFFFF 100%);
|
||
|
|
}
|
||
|
|
|
||
|
|
.stage-card.processing {
|
||
|
|
border-color: #3B82F6;
|
||
|
|
animation: pulse 2s infinite;
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes pulse {
|
||
|
|
0%, 100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4); }
|
||
|
|
50% { box-shadow: 0 0 0 10px rgba(59, 130, 246, 0); }
|
||
|
|
}
|
||
|
|
|
||
|
|
.markdown-body {
|
||
|
|
font-size: 0.875rem;
|
||
|
|
line-height: 1.7;
|
||
|
|
}
|
||
|
|
|
||
|
|
.markdown-body pre {
|
||
|
|
background: #f6f8fa;
|
||
|
|
padding: 1rem;
|
||
|
|
border-radius: 0.5rem;
|
||
|
|
margin: 0.5rem 0;
|
||
|
|
overflow-x: auto;
|
||
|
|
border: 1px solid #e1e4e8;
|
||
|
|
}
|
||
|
|
|
||
|
|
.markdown-body code {
|
||
|
|
font-family: 'Consolas', 'Monaco', monospace;
|
||
|
|
color: #24292e;
|
||
|
|
}
|
||
|
|
|
||
|
|
.markdown-body h1, .markdown-body h2, .markdown-body h3 {
|
||
|
|
margin-top: 1rem;
|
||
|
|
margin-bottom: 0.5rem;
|
||
|
|
font-weight: 600;
|
||
|
|
}
|
||
|
|
|
||
|
|
.markdown-body ul, .markdown-body ol {
|
||
|
|
padding-left: 1.5rem;
|
||
|
|
margin: 0.5rem 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* SSE 连接状态指示器 */
|
||
|
|
.connection-status {
|
||
|
|
display: inline-block;
|
||
|
|
width: 8px;
|
||
|
|
height: 8px;
|
||
|
|
border-radius: 50%;
|
||
|
|
margin-right: 0.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.connection-connected {
|
||
|
|
background-color: #10B981;
|
||
|
|
}
|
||
|
|
|
||
|
|
.connection-disconnected {
|
||
|
|
background-color: #EF4444;
|
||
|
|
}
|
||
|
|
|
||
|
|
.connection-connecting {
|
||
|
|
background-color: #F59E0B;
|
||
|
|
animation: blink 1s infinite;
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes blink {
|
||
|
|
0%, 100% { opacity: 1; }
|
||
|
|
50% { opacity: 0.3; }
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body class="bg-gray-50 min-h-screen">
|
||
|
|
<div id="app" class="min-h-screen">
|
||
|
|
<!-- 顶部导航栏 -->
|
||
|
|
<header class="bg-white shadow-sm border-b border-gray-200">
|
||
|
|
<div class="max-w-7xl mx-auto px-4 py-4 sm:px-6 lg:px-8">
|
||
|
|
<div class="flex items-center justify-between">
|
||
|
|
<div class="flex items-center">
|
||
|
|
<div class="flex-shrink-0">
|
||
|
|
<svg class="h-8 w-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||
|
|
</svg>
|
||
|
|
</div>
|
||
|
|
<div class="ml-4">
|
||
|
|
<h1 class="text-xl font-bold text-gray-900">SDLC Agent Demo</h1>
|
||
|
|
<p class="text-sm text-gray-500">多智能体端到端软件交付协同系统</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="flex items-center space-x-4">
|
||
|
|
<div class="flex items-center text-sm">
|
||
|
|
<span :class="['connection-status', connectionStatusClass]"></span>
|
||
|
|
<span class="text-gray-600">{{ connectionStatusText }}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</header>
|
||
|
|
|
||
|
|
<!-- 主内容区 -->
|
||
|
|
<main class="max-w-7xl mx-auto px-4 py-8 sm:px-6 lg:px-8">
|
||
|
|
<!-- 需求输入区 -->
|
||
|
|
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
|
||
|
|
<h2 class="text-lg font-semibold text-gray-900 mb-4">1. 输入软件需求</h2>
|
||
|
|
<div class="space-y-4">
|
||
|
|
<textarea
|
||
|
|
v-model="requirement"
|
||
|
|
rows="5"
|
||
|
|
placeholder="请输入您的软件需求描述,例如: 开发一个用户管理系统,支持用户的增删改查功能,需要包含以下特性: - 用户注册和登录 - 用户信息管理 - 角色权限控制 - 操作日志记录"
|
||
|
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
|
||
|
|
:disabled="isProcessing"
|
||
|
|
></textarea>
|
||
|
|
<div class="flex items-center justify-between">
|
||
|
|
<p class="text-sm text-gray-500">
|
||
|
|
当前任务 ID: {{ taskId || '无' }}
|
||
|
|
</p>
|
||
|
|
<button
|
||
|
|
@click="startSDLCProcess"
|
||
|
|
:disabled="!canStart || isProcessing"
|
||
|
|
:class="[
|
||
|
|
'px-6 py-2.5 rounded-lg font-medium text-white transition-all duration-200',
|
||
|
|
canStart && !isProcessing
|
||
|
|
? 'bg-blue-600 hover:bg-blue-700 shadow-md hover:shadow-lg'
|
||
|
|
: 'bg-gray-400 cursor-not-allowed'
|
||
|
|
]"
|
||
|
|
>
|
||
|
|
{{ isProcessing ? '执行中...' : '开始执行' }}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 进度展示区 -->
|
||
|
|
<div class="mb-6" v-show="stages.length > 0">
|
||
|
|
<h2 class="text-lg font-semibold text-gray-900 mb-4">2. 执行进度</h2>
|
||
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||
|
|
<div
|
||
|
|
v-for="(stage, index) in stages"
|
||
|
|
:key="stage.id"
|
||
|
|
:class="[
|
||
|
|
'stage-card rounded-lg border-2 p-4 bg-white',
|
||
|
|
{ 'active': stage.status === 'processing' },
|
||
|
|
{ 'completed': stage.status === 'completed' },
|
||
|
|
{ 'processing': stage.status === 'processing' }
|
||
|
|
]"
|
||
|
|
>
|
||
|
|
<div class="flex items-center mb-2">
|
||
|
|
<div
|
||
|
|
:class="[
|
||
|
|
'w-8 h-8 rounded-full flex items-center justify-center mr-3',
|
||
|
|
getStageIconClass(stage.status)
|
||
|
|
]"
|
||
|
|
>
|
||
|
|
<span class="text-white font-bold text-sm">{{ index + 1 }}</span>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<h3 class="font-medium text-gray-900">{{ stage.name }}</h3>
|
||
|
|
<p class="text-xs text-gray-500">{{ stage.agent }}</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="mt-3">
|
||
|
|
<span :class="[
|
||
|
|
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
|
||
|
|
getStageBadgeClass(stage.status)
|
||
|
|
]">
|
||
|
|
{{ getStageStatusText(stage.status) }}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 实时日志区 -->
|
||
|
|
<div class="bg-white rounded-lg shadow-md p-6 mb-6" v-show="logs.length > 0">
|
||
|
|
<div class="flex items-center justify-between mb-4">
|
||
|
|
<h2 class="text-lg font-semibold text-gray-900">3. 实时日志</h2>
|
||
|
|
<button
|
||
|
|
@click="clearLogs"
|
||
|
|
class="text-sm text-gray-500 hover:text-gray-700"
|
||
|
|
>
|
||
|
|
清空日志
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
<div class="bg-gray-900 rounded-lg p-4 h-64 overflow-y-auto font-mono text-sm">
|
||
|
|
<div v-for="(log, index) in logs" :key="index" class="mb-1">
|
||
|
|
<span class="text-gray-500">{{ log.timestamp }}</span>
|
||
|
|
<span :class="getLogLevelClass(log.event)" class="ml-2">[{{ log.event }}]</span>
|
||
|
|
<span class="text-gray-300 ml-2">{{ log.message }}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 结果展示区 -->
|
||
|
|
<div class="space-y-6" v-show="results.length > 0">
|
||
|
|
<div class="flex items-center justify-between">
|
||
|
|
<h2 class="text-lg font-semibold text-gray-900">4. 输出结果</h2>
|
||
|
|
<button
|
||
|
|
@click="downloadResults"
|
||
|
|
:disabled="!isCompleted"
|
||
|
|
:class="[
|
||
|
|
'px-4 py-2 rounded-lg font-medium text-white transition-all duration-200 flex items-center',
|
||
|
|
isCompleted
|
||
|
|
? 'bg-green-600 hover:bg-green-700 shadow-md'
|
||
|
|
: 'bg-gray-400 cursor-not-allowed'
|
||
|
|
]"
|
||
|
|
>
|
||
|
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
|
||
|
|
</svg>
|
||
|
|
{{ isCompleted ? '打包下载结果' : '执行完成后下载' }}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div v-for="(result, index) in results" :key="index" class="bg-white rounded-lg shadow-md overflow-hidden">
|
||
|
|
<div
|
||
|
|
@click="result.expanded = !result.expanded"
|
||
|
|
class="px-6 py-4 bg-gray-50 border-b border-gray-200 cursor-pointer hover:bg-gray-100 transition-colors"
|
||
|
|
>
|
||
|
|
<div class="flex items-center justify-between">
|
||
|
|
<div class="flex items-center">
|
||
|
|
<svg
|
||
|
|
:class="['w-5 h-5 mr-2 text-gray-500 transform transition-transform', result.expanded ? 'rotate-90' : '']"
|
||
|
|
fill="none"
|
||
|
|
stroke="currentColor"
|
||
|
|
viewBox="0 0 24 24"
|
||
|
|
>
|
||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||
|
|
</svg>
|
||
|
|
<h3 class="font-semibold text-gray-900">{{ result.title }}</h3>
|
||
|
|
</div>
|
||
|
|
<div class="flex items-center space-x-2">
|
||
|
|
<span class="text-xs text-gray-500">{{ formatDate(result.timestamp) }}</span>
|
||
|
|
<button
|
||
|
|
@click.stop="copyToClipboard(result.content)"
|
||
|
|
class="text-sm text-blue-600 hover:text-blue-700"
|
||
|
|
>
|
||
|
|
复制
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div v-show="result.expanded" class="p-6">
|
||
|
|
<div class="markdown-body" v-html="renderMarkdown(result.content)"></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</main>
|
||
|
|
|
||
|
|
<!-- 页脚 -->
|
||
|
|
<footer class="bg-white border-t border-gray-200 mt-12">
|
||
|
|
<div class="max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8">
|
||
|
|
<p class="text-center text-sm text-gray-500">
|
||
|
|
基于 CrewAI + Qwen3.5-flash + FastAPI(SSE) 构建 | Bosch Demo
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</footer>
|
||
|
|
|
||
|
|
<!-- 复制成功提示 -->
|
||
|
|
<div v-if="showCopyToast" class="fixed bottom-4 right-4 bg-green-600 text-white px-6 py-3 rounded-lg shadow-lg transition-opacity duration-300">
|
||
|
|
✓ 已复制到剪贴板
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
const { createApp } = Vue;
|
||
|
|
|
||
|
|
createApp({
|
||
|
|
data() {
|
||
|
|
return {
|
||
|
|
// 需求输入
|
||
|
|
requirement: '',
|
||
|
|
|
||
|
|
// 任务管理
|
||
|
|
taskId: null,
|
||
|
|
isProcessing: false,
|
||
|
|
|
||
|
|
// SSE 连接
|
||
|
|
eventSource: null,
|
||
|
|
connectionStatus: 'disconnected', // 'connecting', 'connected', 'disconnected'
|
||
|
|
|
||
|
|
// 阶段定义
|
||
|
|
stages: [],
|
||
|
|
|
||
|
|
// 实时日志
|
||
|
|
logs: [],
|
||
|
|
|
||
|
|
// 结果数据
|
||
|
|
results: [],
|
||
|
|
|
||
|
|
// UI 状态
|
||
|
|
showCopyToast: false
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
computed: {
|
||
|
|
canStart() {
|
||
|
|
return this.requirement.trim().length >= 10 && !this.isProcessing;
|
||
|
|
},
|
||
|
|
|
||
|
|
connectionStatusClass() {
|
||
|
|
const statusMap = {
|
||
|
|
'connecting': 'connection-connecting',
|
||
|
|
'connected': 'connection-connected',
|
||
|
|
'disconnected': 'connection-disconnected'
|
||
|
|
};
|
||
|
|
return statusMap[this.connectionStatus];
|
||
|
|
},
|
||
|
|
|
||
|
|
connectionStatusText() {
|
||
|
|
const textMap = {
|
||
|
|
'connecting': '连接中...',
|
||
|
|
'connected': '已连接',
|
||
|
|
'disconnected': '未连接'
|
||
|
|
};
|
||
|
|
return textMap[this.connectionStatus];
|
||
|
|
},
|
||
|
|
|
||
|
|
isCompleted() {
|
||
|
|
return this.stages.length > 0 &&
|
||
|
|
this.stages.every(s => s.status === 'completed');
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
methods: {
|
||
|
|
/**
|
||
|
|
* 下载打包结果
|
||
|
|
*/
|
||
|
|
downloadResults() {
|
||
|
|
if (!this.taskId || !this.isCompleted) return;
|
||
|
|
|
||
|
|
const url = `/api/v1/sdlc/download/${this.taskId}`;
|
||
|
|
window.open(url, '_blank');
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 启动 SDLC 流程
|
||
|
|
*/
|
||
|
|
async startSDLCProcess() {
|
||
|
|
if (!this.canStart) return;
|
||
|
|
|
||
|
|
try {
|
||
|
|
this.isProcessing = true;
|
||
|
|
this.stages = [];
|
||
|
|
this.logs = [];
|
||
|
|
this.results = [];
|
||
|
|
|
||
|
|
// 调用 API 启动任务
|
||
|
|
const response = await fetch('/api/v1/sdlc/start', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json'
|
||
|
|
},
|
||
|
|
body: JSON.stringify({
|
||
|
|
requirement: this.requirement
|
||
|
|
})
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
const data = await response.json();
|
||
|
|
this.taskId = data.task_id;
|
||
|
|
this.addLog('system', '任务已启动', `Task ID: ${data.task_id}`);
|
||
|
|
|
||
|
|
// 初始化阶段
|
||
|
|
this.initStages();
|
||
|
|
|
||
|
|
// 连接 SSE
|
||
|
|
this.connectSSE(data.task_id);
|
||
|
|
|
||
|
|
} catch (error) {
|
||
|
|
console.error('启动失败:', error);
|
||
|
|
this.addLog('error', '启动失败', error.message);
|
||
|
|
this.isProcessing = false;
|
||
|
|
alert(`启动失败:${error.message}`);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 初始化阶段
|
||
|
|
*/
|
||
|
|
initStages() {
|
||
|
|
this.stages = [
|
||
|
|
{ id: 'pm', name: '需求分析', agent: 'PM Agent', status: 'pending' },
|
||
|
|
{ id: 'qa', name: '测试设计', agent: 'QA Agent', status: 'pending' },
|
||
|
|
{ id: 'dev', name: '代码实现', agent: 'Dev Agent', status: 'pending' },
|
||
|
|
{ id: 'final', name: '交付完成', agent: 'Orchestrator', status: 'pending' }
|
||
|
|
];
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 连接 SSE
|
||
|
|
*/
|
||
|
|
connectSSE(taskId) {
|
||
|
|
const url = `/api/v1/sdlc/stream/${taskId}`;
|
||
|
|
|
||
|
|
this.connectionStatus = 'connecting';
|
||
|
|
this.addLog('system', 'SSE', `连接到:${url}`);
|
||
|
|
|
||
|
|
this.eventSource = new EventSource(url);
|
||
|
|
|
||
|
|
// 连接成功
|
||
|
|
this.eventSource.onopen = () => {
|
||
|
|
this.connectionStatus = 'connected';
|
||
|
|
this.addLog('system', 'SSE', '连接成功');
|
||
|
|
};
|
||
|
|
|
||
|
|
// PM 阶段
|
||
|
|
this.eventSource.addEventListener('pm_start', (event) => {
|
||
|
|
const data = JSON.parse(event.data);
|
||
|
|
this.updateStageStatus('pm', 'processing');
|
||
|
|
this.addLog('pm_start', 'PM Agent', '开始需求分析...');
|
||
|
|
});
|
||
|
|
|
||
|
|
this.eventSource.addEventListener('pm_complete', (event) => {
|
||
|
|
const data = JSON.parse(event.data);
|
||
|
|
this.updateStageStatus('pm', 'completed');
|
||
|
|
this.addLog('pm_complete', 'PM Agent', '需求分析完成');
|
||
|
|
this.addResult('📋 软件需求规格说明书 (SRS)', data.content, data.timestamp);
|
||
|
|
});
|
||
|
|
|
||
|
|
// QA 阶段
|
||
|
|
this.eventSource.addEventListener('qa_start', (event) => {
|
||
|
|
const data = JSON.parse(event.data);
|
||
|
|
this.updateStageStatus('qa', 'processing');
|
||
|
|
this.addLog('qa_start', 'QA Agent', '开始测试用例设计...');
|
||
|
|
});
|
||
|
|
|
||
|
|
this.eventSource.addEventListener('qa_complete', (event) => {
|
||
|
|
const data = JSON.parse(event.data);
|
||
|
|
this.updateStageStatus('qa', 'completed');
|
||
|
|
this.addLog('qa_complete', 'QA Agent', '测试用例设计完成');
|
||
|
|
this.addResult('🧪 测试方案与用例', data.content, data.timestamp);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Dev 阶段
|
||
|
|
this.eventSource.addEventListener('dev_start', (event) => {
|
||
|
|
const data = JSON.parse(event.data);
|
||
|
|
this.updateStageStatus('dev', 'processing');
|
||
|
|
this.addLog('dev_start', 'Dev Agent', '开始代码实现...');
|
||
|
|
});
|
||
|
|
|
||
|
|
this.eventSource.addEventListener('dev_complete', (event) => {
|
||
|
|
const data = JSON.parse(event.data);
|
||
|
|
this.updateStageStatus('dev', 'completed');
|
||
|
|
this.addLog('dev_complete', 'Dev Agent', '代码实现完成');
|
||
|
|
this.addResult('💻 代码实现', data.content, data.timestamp);
|
||
|
|
});
|
||
|
|
|
||
|
|
// 最终结果
|
||
|
|
this.eventSource.addEventListener('final_result', (event) => {
|
||
|
|
const data = JSON.parse(event.data);
|
||
|
|
this.updateStageStatus('final', 'completed');
|
||
|
|
this.addLog('final_result', 'System', 'SDLC 流程完成');
|
||
|
|
this.isProcessing = false;
|
||
|
|
this.connectionStatus = 'disconnected';
|
||
|
|
});
|
||
|
|
|
||
|
|
// 错误处理
|
||
|
|
this.eventSource.addEventListener('error', (event) => {
|
||
|
|
const data = JSON.parse(event.data);
|
||
|
|
this.addLog('error', 'Error', data.error || '未知错误');
|
||
|
|
this.isProcessing = false;
|
||
|
|
this.connectionStatus = 'disconnected';
|
||
|
|
alert(`执行错误:${data.error}`);
|
||
|
|
});
|
||
|
|
|
||
|
|
// 连接错误
|
||
|
|
this.eventSource.onerror = () => {
|
||
|
|
this.addLog('system', 'SSE', '连接断开');
|
||
|
|
this.connectionStatus = 'disconnected';
|
||
|
|
this.eventSource.close();
|
||
|
|
};
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 更新阶段状态
|
||
|
|
*/
|
||
|
|
updateStageStatus(stageId, status) {
|
||
|
|
const stage = this.stages.find(s => s.id === stageId);
|
||
|
|
if (stage) {
|
||
|
|
stage.status = status;
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 添加日志
|
||
|
|
*/
|
||
|
|
addLog(event, source, message) {
|
||
|
|
this.logs.push({
|
||
|
|
timestamp: new Date().toLocaleTimeString('zh-CN'),
|
||
|
|
event,
|
||
|
|
source,
|
||
|
|
message
|
||
|
|
});
|
||
|
|
// 保持最新 100 条日志
|
||
|
|
if (this.logs.length > 100) {
|
||
|
|
this.logs.shift();
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 添加结果
|
||
|
|
*/
|
||
|
|
addResult(title, content, timestamp) {
|
||
|
|
this.results.push({
|
||
|
|
title,
|
||
|
|
content,
|
||
|
|
timestamp,
|
||
|
|
expanded: true
|
||
|
|
});
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 清空日志
|
||
|
|
*/
|
||
|
|
clearLogs() {
|
||
|
|
this.logs = [];
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 获取阶段图标样式
|
||
|
|
*/
|
||
|
|
getStageIconClass(status) {
|
||
|
|
const classMap = {
|
||
|
|
'pending': 'bg-gray-400',
|
||
|
|
'processing': 'bg-blue-500',
|
||
|
|
'completed': 'bg-green-500'
|
||
|
|
};
|
||
|
|
return classMap[status] || classMap['pending'];
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 获取阶段徽章样式
|
||
|
|
*/
|
||
|
|
getStageBadgeClass(status) {
|
||
|
|
const classMap = {
|
||
|
|
'pending': 'bg-gray-100 text-gray-800',
|
||
|
|
'processing': 'bg-blue-100 text-blue-800',
|
||
|
|
'completed': 'bg-green-100 text-green-800'
|
||
|
|
};
|
||
|
|
return classMap[status] || classMap['pending'];
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 获取阶段状态文本
|
||
|
|
*/
|
||
|
|
getStageStatusText(status) {
|
||
|
|
const textMap = {
|
||
|
|
'pending': '等待中',
|
||
|
|
'processing': '进行中',
|
||
|
|
'completed': '已完成'
|
||
|
|
};
|
||
|
|
return textMap[status] || status;
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 获取日志级别样式
|
||
|
|
*/
|
||
|
|
getLogLevelClass(event) {
|
||
|
|
const classMap = {
|
||
|
|
'pm_start': 'text-blue-400',
|
||
|
|
'pm_complete': 'text-green-400',
|
||
|
|
'qa_start': 'text-blue-400',
|
||
|
|
'qa_complete': 'text-green-400',
|
||
|
|
'dev_start': 'text-blue-400',
|
||
|
|
'dev_complete': 'text-green-400',
|
||
|
|
'final_result': 'text-purple-400',
|
||
|
|
'error': 'text-red-400',
|
||
|
|
'system': 'text-yellow-400'
|
||
|
|
};
|
||
|
|
return classMap[event] || 'text-gray-400';
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 渲染 Markdown
|
||
|
|
*/
|
||
|
|
renderMarkdown(content) {
|
||
|
|
if (!content) return '';
|
||
|
|
return marked.parse(content);
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 格式化日期
|
||
|
|
*/
|
||
|
|
formatDate(timestamp) {
|
||
|
|
if (!timestamp) return '';
|
||
|
|
try {
|
||
|
|
return new Date(timestamp).toLocaleString('zh-CN');
|
||
|
|
} catch {
|
||
|
|
return timestamp;
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 复制到剪贴板
|
||
|
|
*/
|
||
|
|
async copyToClipboard(content) {
|
||
|
|
try {
|
||
|
|
await navigator.clipboard.writeText(content);
|
||
|
|
this.showCopyToast = true;
|
||
|
|
setTimeout(() => {
|
||
|
|
this.showCopyToast = false;
|
||
|
|
}, 2000);
|
||
|
|
} catch (err) {
|
||
|
|
console.error('复制失败:', err);
|
||
|
|
alert('复制失败,请手动复制');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
beforeUnmount() {
|
||
|
|
// 清理 SSE 连接
|
||
|
|
if (this.eventSource) {
|
||
|
|
this.eventSource.close();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}).mount('#app');
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|