2025-04-28 12:25:20 +08:00
|
|
|
|
class RunSimulation extends HTMLElement {
|
|
|
|
|
constructor() {
|
|
|
|
|
super();
|
|
|
|
|
this.attachShadow({ mode: 'open' });
|
|
|
|
|
this.scenarioFiles = [];
|
|
|
|
|
this.currentScenario = null;
|
|
|
|
|
this.modelGroups = [];
|
|
|
|
|
this.services = [];
|
|
|
|
|
this.currentSimulationId = null;
|
|
|
|
|
this.eventSource = null;
|
2025-05-14 16:07:13 +08:00
|
|
|
|
this.reconnectAttempts = 0;
|
|
|
|
|
this.maxReconnectAttempts = 3;
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 添加reactivate方法,用于从缓存中恢复时检查仿真状态
|
|
|
|
|
async reactivate() {
|
|
|
|
|
// 确保先加载场景文件列表
|
|
|
|
|
await this.fetchScenarioFiles();
|
|
|
|
|
|
|
|
|
|
// 如果之前已经选择了配置文件,恢复配置文件下拉框的选中状态
|
|
|
|
|
if (this.currentScenario) {
|
|
|
|
|
const scenarioSelect = this.shadowRoot.querySelector('#scenario-select');
|
|
|
|
|
if (scenarioSelect) {
|
|
|
|
|
// 检查所选配置文件是否在列表中
|
|
|
|
|
const fileExists = Array.from(scenarioSelect.options).some(opt => opt.value === this.currentScenario);
|
|
|
|
|
|
|
|
|
|
if (fileExists) {
|
|
|
|
|
scenarioSelect.value = this.currentScenario;
|
|
|
|
|
|
|
|
|
|
// 恢复模型和服务列表显示
|
|
|
|
|
this.updateModelAndServiceLists();
|
|
|
|
|
} else {
|
|
|
|
|
console.warn('之前选择的配置文件不再可用:', this.currentScenario);
|
|
|
|
|
this.currentScenario = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-14 16:07:13 +08:00
|
|
|
|
// 检查是否有XNEngine进程在运行
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch('/api/check-xnengine');
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
if (data.running) {
|
|
|
|
|
this.showMessage('检测到正在运行的仿真,正在重新连接...');
|
|
|
|
|
|
|
|
|
|
// 更新UI以反映运行状态
|
|
|
|
|
const runButton = this.shadowRoot.querySelector('#run-button');
|
|
|
|
|
if (runButton) {
|
|
|
|
|
runButton.disabled = true;
|
|
|
|
|
runButton.textContent = '运行中...';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 显示终止按钮
|
|
|
|
|
const stopButton = this.shadowRoot.querySelector('#stop-button');
|
|
|
|
|
if (stopButton) {
|
|
|
|
|
stopButton.style.display = 'block';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 使用进程ID作为仿真ID重新连接
|
|
|
|
|
this.currentSimulationId = data.pid.toString();
|
|
|
|
|
this.connectToEventSource(this.currentSimulationId);
|
|
|
|
|
|
|
|
|
|
// 清空并初始化输出框
|
|
|
|
|
const outputContent = this.shadowRoot.querySelector('#output-content');
|
|
|
|
|
outputContent.innerHTML = '重新连接到运行中的仿真...\n';
|
|
|
|
|
} else {
|
|
|
|
|
// 没有运行的XNEngine进程,重置UI
|
|
|
|
|
this.resetUIAfterCompletion();
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('检查XNEngine进程失败:', error);
|
|
|
|
|
this.showError('检查仿真状态失败: ' + error.message);
|
|
|
|
|
this.resetUIAfterCompletion();
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查仿真状态
|
|
|
|
|
async checkSimulationStatus(simulationId) {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`/api/simulation-status/${simulationId}`);
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
if (data.running) {
|
|
|
|
|
// 仿真仍在运行,重新连接到事件源
|
|
|
|
|
this.showMessage('重新连接到运行中的仿真...');
|
|
|
|
|
|
|
|
|
|
// 更新UI以反映运行状态
|
|
|
|
|
const runButton = this.shadowRoot.querySelector('#run-button');
|
|
|
|
|
if (runButton) {
|
|
|
|
|
runButton.disabled = true;
|
|
|
|
|
runButton.textContent = '运行中...';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 显示终止按钮
|
|
|
|
|
const stopButton = this.shadowRoot.querySelector('#stop-button');
|
|
|
|
|
if (stopButton) {
|
|
|
|
|
stopButton.style.display = 'block';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 重新连接到事件源
|
|
|
|
|
this.connectToEventSource(simulationId);
|
|
|
|
|
} else {
|
|
|
|
|
// 仿真已经停止,重置UI
|
|
|
|
|
this.showMessage('仿真已结束');
|
|
|
|
|
this.resetUIAfterCompletion();
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('检查仿真状态失败:', error);
|
|
|
|
|
// 假设仿真已停止
|
|
|
|
|
this.resetUIAfterCompletion();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
connectedCallback() {
|
2025-05-14 16:07:13 +08:00
|
|
|
|
// 先加载场景文件列表
|
2025-04-28 12:25:20 +08:00
|
|
|
|
this.fetchScenarioFiles();
|
2025-05-14 16:07:13 +08:00
|
|
|
|
// 然后渲染UI
|
2025-04-28 12:25:20 +08:00
|
|
|
|
this.render();
|
2025-05-14 16:07:13 +08:00
|
|
|
|
|
|
|
|
|
// 最后检查是否有XNEngine进程在运行
|
|
|
|
|
this.checkAndConnectToExistingSimulation();
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fetchScenarioFiles() {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch('/api/scenario-files');
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error('无法获取场景文件');
|
|
|
|
|
}
|
|
|
|
|
this.scenarioFiles = await response.json();
|
|
|
|
|
this.updateScenarioDropdown();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取场景文件失败:', error);
|
|
|
|
|
this.showError('获取场景文件失败: ' + error.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateScenarioDropdown() {
|
|
|
|
|
const dropdown = this.shadowRoot.querySelector('#scenario-select');
|
|
|
|
|
if (!dropdown) return;
|
|
|
|
|
|
|
|
|
|
// 清空现有选项
|
|
|
|
|
dropdown.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
// 添加提示选项
|
|
|
|
|
const defaultOption = document.createElement('option');
|
|
|
|
|
defaultOption.value = '';
|
|
|
|
|
defaultOption.textContent = '-- 选择配置文件 --';
|
|
|
|
|
defaultOption.disabled = true;
|
|
|
|
|
defaultOption.selected = true;
|
|
|
|
|
dropdown.appendChild(defaultOption);
|
|
|
|
|
|
|
|
|
|
// 添加场景文件选项
|
|
|
|
|
this.scenarioFiles.forEach(file => {
|
|
|
|
|
const option = document.createElement('option');
|
|
|
|
|
option.value = file.path;
|
|
|
|
|
option.textContent = file.name;
|
|
|
|
|
dropdown.appendChild(option);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async onScenarioSelected(event) {
|
|
|
|
|
const selectedScenarioPath = event.target.value;
|
|
|
|
|
if (!selectedScenarioPath) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
this.showMessage('加载配置文件内容...');
|
|
|
|
|
// 使用file-content API获取XML内容
|
|
|
|
|
const response = await fetch(`/api/file-content?path=${encodeURIComponent(selectedScenarioPath)}`);
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error('无法获取配置文件内容');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const xmlContent = await response.text();
|
|
|
|
|
this.parseScenarioXML(xmlContent);
|
|
|
|
|
|
|
|
|
|
this.currentScenario = selectedScenarioPath;
|
|
|
|
|
this.updateModelAndServiceLists();
|
|
|
|
|
this.showMessage('配置文件加载完成');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('加载配置文件失败:', error);
|
|
|
|
|
this.showError('加载配置文件失败: ' + error.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
parseScenarioXML(xmlContent) {
|
|
|
|
|
try {
|
|
|
|
|
const parser = new DOMParser();
|
|
|
|
|
const xmlDoc = parser.parseFromString(xmlContent, "text/xml");
|
|
|
|
|
|
|
|
|
|
// 解析模型组和模型
|
|
|
|
|
this.modelGroups = [];
|
|
|
|
|
const modelGroupElements = xmlDoc.querySelectorAll('ModelGroup');
|
|
|
|
|
modelGroupElements.forEach(groupElem => {
|
|
|
|
|
const groupName = groupElem.getAttribute('Name');
|
|
|
|
|
const models = [];
|
|
|
|
|
|
|
|
|
|
groupElem.querySelectorAll('Model').forEach(modelElem => {
|
|
|
|
|
models.push({
|
|
|
|
|
name: modelElem.getAttribute('Name'),
|
|
|
|
|
className: modelElem.getAttribute('ClassName')
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.modelGroups.push({
|
|
|
|
|
name: groupName,
|
|
|
|
|
models: models
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 解析服务
|
|
|
|
|
this.services = [];
|
|
|
|
|
const serviceElements = xmlDoc.querySelectorAll('ServicesList > Service');
|
|
|
|
|
serviceElements.forEach(serviceElem => {
|
|
|
|
|
this.services.push({
|
|
|
|
|
name: serviceElem.getAttribute('Name'),
|
|
|
|
|
className: serviceElem.getAttribute('ClassName')
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('解析XML失败:', error);
|
|
|
|
|
throw new Error('解析XML失败: ' + error.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateModelAndServiceLists() {
|
|
|
|
|
// 更新模型列表
|
|
|
|
|
const modelListContainer = this.shadowRoot.querySelector('#model-list');
|
|
|
|
|
modelListContainer.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
this.modelGroups.forEach(group => {
|
|
|
|
|
const groupItem = document.createElement('div');
|
|
|
|
|
groupItem.className = 'list-group';
|
|
|
|
|
|
|
|
|
|
const groupHeader = document.createElement('div');
|
|
|
|
|
groupHeader.className = 'list-group-header';
|
|
|
|
|
groupHeader.textContent = group.name;
|
|
|
|
|
groupItem.appendChild(groupHeader);
|
|
|
|
|
|
|
|
|
|
const modelsList = document.createElement('div');
|
|
|
|
|
modelsList.className = 'list-items';
|
|
|
|
|
|
|
|
|
|
group.models.forEach(model => {
|
|
|
|
|
const modelItem = document.createElement('div');
|
|
|
|
|
modelItem.className = 'list-item';
|
|
|
|
|
modelItem.textContent = model.name;
|
|
|
|
|
modelsList.appendChild(modelItem);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
groupItem.appendChild(modelsList);
|
|
|
|
|
modelListContainer.appendChild(groupItem);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 更新服务列表
|
|
|
|
|
const serviceListContainer = this.shadowRoot.querySelector('#service-list');
|
|
|
|
|
serviceListContainer.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
this.services.forEach(service => {
|
|
|
|
|
const serviceItem = document.createElement('div');
|
|
|
|
|
serviceItem.className = 'list-item';
|
|
|
|
|
serviceItem.textContent = service.name;
|
|
|
|
|
serviceListContainer.appendChild(serviceItem);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ANSI终端颜色转换为HTML
|
|
|
|
|
convertAnsiToHtml(text) {
|
|
|
|
|
if (!text) return '';
|
|
|
|
|
|
|
|
|
|
// 映射ANSI颜色代码到CSS类名
|
|
|
|
|
const ansiToHtmlClass = {
|
|
|
|
|
// 前景色 (30-37)
|
|
|
|
|
30: 'ansi-black',
|
|
|
|
|
31: 'ansi-red',
|
|
|
|
|
32: 'ansi-green',
|
|
|
|
|
33: 'ansi-yellow',
|
|
|
|
|
34: 'ansi-blue',
|
|
|
|
|
35: 'ansi-magenta',
|
|
|
|
|
36: 'ansi-cyan',
|
|
|
|
|
37: 'ansi-white',
|
|
|
|
|
|
|
|
|
|
// 明亮前景色 (90-97)
|
|
|
|
|
90: 'ansi-bright-black',
|
|
|
|
|
91: 'ansi-bright-red',
|
|
|
|
|
92: 'ansi-bright-green',
|
|
|
|
|
93: 'ansi-bright-yellow',
|
|
|
|
|
94: 'ansi-bright-blue',
|
|
|
|
|
95: 'ansi-bright-magenta',
|
|
|
|
|
96: 'ansi-bright-cyan',
|
|
|
|
|
97: 'ansi-bright-white',
|
|
|
|
|
|
|
|
|
|
// 背景色 (40-47)
|
|
|
|
|
40: 'ansi-bg-black',
|
|
|
|
|
41: 'ansi-bg-red',
|
|
|
|
|
42: 'ansi-bg-green',
|
|
|
|
|
43: 'ansi-bg-yellow',
|
|
|
|
|
44: 'ansi-bg-blue',
|
|
|
|
|
45: 'ansi-bg-magenta',
|
|
|
|
|
46: 'ansi-bg-cyan',
|
|
|
|
|
47: 'ansi-bg-white',
|
|
|
|
|
|
|
|
|
|
// 文字样式
|
|
|
|
|
1: 'ansi-bold',
|
|
|
|
|
3: 'ansi-italic',
|
|
|
|
|
4: 'ansi-underline'
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 替换ANSI转义序列
|
|
|
|
|
const parts = [];
|
|
|
|
|
let currentSpan = null;
|
|
|
|
|
let currentClasses = [];
|
|
|
|
|
|
|
|
|
|
// 先分割ANSI序列和文本
|
|
|
|
|
const regex = /\x1b\[([\d;]*)m/g;
|
|
|
|
|
let lastIndex = 0;
|
|
|
|
|
let match;
|
|
|
|
|
|
|
|
|
|
while ((match = regex.exec(text)) !== null) {
|
|
|
|
|
// 添加匹配前的文本
|
|
|
|
|
if (match.index > lastIndex) {
|
|
|
|
|
parts.push({
|
|
|
|
|
text: text.substring(lastIndex, match.index),
|
|
|
|
|
classes: [...currentClasses]
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理ANSI代码
|
|
|
|
|
const codes = match[1].split(';').map(Number);
|
|
|
|
|
|
|
|
|
|
// 重置所有样式
|
|
|
|
|
if (codes.includes(0) || codes.length === 0) {
|
|
|
|
|
currentClasses = [];
|
|
|
|
|
} else {
|
|
|
|
|
// 添加样式
|
|
|
|
|
for (const code of codes) {
|
|
|
|
|
if (ansiToHtmlClass[code]) {
|
|
|
|
|
currentClasses.push(ansiToHtmlClass[code]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lastIndex = regex.lastIndex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 添加最后一段文本
|
|
|
|
|
if (lastIndex < text.length) {
|
|
|
|
|
parts.push({
|
|
|
|
|
text: text.substring(lastIndex),
|
|
|
|
|
classes: [...currentClasses]
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 生成HTML
|
|
|
|
|
let html = '';
|
|
|
|
|
for (const part of parts) {
|
|
|
|
|
if (part.text) {
|
|
|
|
|
if (part.classes.length > 0) {
|
|
|
|
|
html += `<span class="${part.classes.join(' ')}">${this.escapeHtml(part.text)}</span>`;
|
|
|
|
|
} else {
|
|
|
|
|
html += this.escapeHtml(part.text);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return html;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 转义HTML特殊字符
|
|
|
|
|
escapeHtml(text) {
|
|
|
|
|
return text
|
|
|
|
|
.replace(/&/g, '&')
|
|
|
|
|
.replace(/</g, '<')
|
|
|
|
|
.replace(/>/g, '>')
|
|
|
|
|
.replace(/"/g, '"')
|
|
|
|
|
.replace(/'/g, ''');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async runTest() {
|
|
|
|
|
const selectedScenario = this.shadowRoot.querySelector('#scenario-select').value;
|
|
|
|
|
if (!selectedScenario) {
|
|
|
|
|
this.showError('请先选择配置文件');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
this.showMessage('准备运行仿真...');
|
|
|
|
|
const runButton = this.shadowRoot.querySelector('#run-button');
|
|
|
|
|
runButton.disabled = true;
|
|
|
|
|
runButton.textContent = '运行中...';
|
|
|
|
|
|
|
|
|
|
// 显示终止按钮
|
|
|
|
|
const stopButton = this.shadowRoot.querySelector('#stop-button');
|
|
|
|
|
stopButton.style.display = 'block';
|
|
|
|
|
|
|
|
|
|
// 清空并初始化输出框
|
|
|
|
|
const outputContent = this.shadowRoot.querySelector('#output-content');
|
|
|
|
|
outputContent.innerHTML = '开始执行仿真...\n';
|
|
|
|
|
|
|
|
|
|
// 关闭之前的EventSource连接
|
|
|
|
|
if (this.eventSource) {
|
|
|
|
|
this.eventSource.close();
|
|
|
|
|
this.eventSource = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 保存当前仿真信息
|
|
|
|
|
this.currentSimulationId = Date.now().toString();
|
|
|
|
|
|
|
|
|
|
// 准备启动参数
|
2025-05-26 17:01:23 +08:00
|
|
|
|
const simulationArgs = ['-f', selectedScenario];
|
2025-04-28 12:25:20 +08:00
|
|
|
|
|
|
|
|
|
// 调用后端API执行仿真
|
|
|
|
|
const response = await fetch('/api/run-simulation', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
id: this.currentSimulationId,
|
|
|
|
|
args: simulationArgs,
|
|
|
|
|
timeout: 120000 // 2分钟超时
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
const errorData = await response.json();
|
|
|
|
|
outputContent.innerHTML += `\n错误: ${errorData.message || '仿真执行失败'}\n`;
|
|
|
|
|
if (errorData.output) outputContent.innerHTML += this.convertAnsiToHtml(errorData.output);
|
|
|
|
|
if (errorData.errorOutput) outputContent.innerHTML += this.convertAnsiToHtml(errorData.errorOutput);
|
|
|
|
|
throw new Error(errorData.message || '仿真执行失败');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const responseData = await response.json();
|
|
|
|
|
|
|
|
|
|
// 获取仿真ID
|
|
|
|
|
const simulationId = responseData.simulationId || this.currentSimulationId;
|
|
|
|
|
this.currentSimulationId = simulationId;
|
|
|
|
|
|
2025-05-14 16:07:13 +08:00
|
|
|
|
// 如果是连接到现有进程,显示相应消息
|
|
|
|
|
if (responseData.isExisting) {
|
|
|
|
|
this.showMessage('已连接到运行中的仿真');
|
|
|
|
|
outputContent.innerHTML = '已连接到运行中的仿真...\n';
|
|
|
|
|
|
|
|
|
|
// 如果有配置文件路径,自动选择
|
|
|
|
|
if (responseData.scenarioFile) {
|
|
|
|
|
const scenarioSelect = this.shadowRoot.querySelector('#scenario-select');
|
|
|
|
|
if (scenarioSelect) {
|
|
|
|
|
// 检查配置文件是否在列表中
|
|
|
|
|
const fileExists = Array.from(scenarioSelect.options).some(opt => opt.value === responseData.scenarioFile);
|
|
|
|
|
if (fileExists) {
|
|
|
|
|
scenarioSelect.value = responseData.scenarioFile;
|
|
|
|
|
this.currentScenario = responseData.scenarioFile;
|
|
|
|
|
// 更新模型和服务列表
|
|
|
|
|
this.updateModelAndServiceLists();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
this.showSuccess(`仿真已启动`);
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-28 12:25:20 +08:00
|
|
|
|
// 设置SSE连接获取实时输出
|
|
|
|
|
this.connectToEventSource(simulationId);
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('执行仿真失败:', error);
|
|
|
|
|
this.showError('执行仿真失败: ' + error.message);
|
|
|
|
|
|
|
|
|
|
// 显示错误详情
|
|
|
|
|
const outputContent = this.shadowRoot.querySelector('#output-content');
|
|
|
|
|
outputContent.innerHTML += `\n\n执行错误: ${error.message}`;
|
|
|
|
|
|
|
|
|
|
// 隐藏终止按钮并重置运行按钮
|
|
|
|
|
const stopButton = this.shadowRoot.querySelector('#stop-button');
|
|
|
|
|
stopButton.style.display = 'none';
|
|
|
|
|
|
|
|
|
|
const runButton = this.shadowRoot.querySelector('#run-button');
|
|
|
|
|
runButton.disabled = false;
|
|
|
|
|
runButton.textContent = '运行仿真';
|
|
|
|
|
|
|
|
|
|
// 清除当前仿真ID和关闭SSE连接
|
|
|
|
|
this.closeEventSource();
|
|
|
|
|
this.currentSimulationId = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 连接到SSE事件源获取实时输出
|
|
|
|
|
connectToEventSource(simulationId) {
|
|
|
|
|
// 关闭之前的连接
|
|
|
|
|
this.closeEventSource();
|
|
|
|
|
|
|
|
|
|
// 创建新的SSE连接
|
|
|
|
|
const url = `/api/simulation-output/${simulationId}`;
|
|
|
|
|
this.eventSource = new EventSource(url);
|
|
|
|
|
|
|
|
|
|
// 标准输出和错误输出
|
|
|
|
|
this.eventSource.addEventListener('output', (event) => {
|
|
|
|
|
try {
|
|
|
|
|
const data = JSON.parse(event.data);
|
|
|
|
|
const outputContent = this.shadowRoot.querySelector('#output-content');
|
|
|
|
|
|
|
|
|
|
// 添加新输出并应用ANSI颜色
|
|
|
|
|
if (data.data) {
|
|
|
|
|
const html = this.convertAnsiToHtml(data.data);
|
|
|
|
|
outputContent.innerHTML += html;
|
|
|
|
|
|
|
|
|
|
// 自动滚动到底部
|
|
|
|
|
outputContent.scrollTop = outputContent.scrollHeight;
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('处理输出事件失败:', error);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 仿真状态
|
|
|
|
|
this.eventSource.addEventListener('status', (event) => {
|
|
|
|
|
try {
|
|
|
|
|
const data = JSON.parse(event.data);
|
|
|
|
|
|
|
|
|
|
if (data.running === false) {
|
|
|
|
|
// 仿真已经不存在或已结束
|
|
|
|
|
this.showMessage(data.message || '仿真不存在或已结束');
|
|
|
|
|
|
2025-05-14 16:07:13 +08:00
|
|
|
|
// 如果是进程不存在,尝试重新连接
|
|
|
|
|
if (data.message === '仿真不存在或已结束' && this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
|
|
|
this.reconnectAttempts++;
|
|
|
|
|
this.showMessage(`尝试重新连接 (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
|
|
|
|
|
setTimeout(() => this.checkAndConnectToExistingSimulation(), 1000);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-28 12:25:20 +08:00
|
|
|
|
// 重置UI
|
|
|
|
|
this.resetUIAfterCompletion();
|
2025-05-14 16:07:13 +08:00
|
|
|
|
} else if (data.running === true) {
|
|
|
|
|
// 更新状态为已连接
|
|
|
|
|
this.showSuccess('已连接到运行中的仿真');
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('处理状态事件失败:', error);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 仿真完成
|
|
|
|
|
this.eventSource.addEventListener('completed', (event) => {
|
|
|
|
|
try {
|
|
|
|
|
const data = JSON.parse(event.data);
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
this.showSuccess('仿真执行成功');
|
|
|
|
|
} else {
|
|
|
|
|
this.showError(`仿真执行失败: ${data.message}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 重置UI
|
|
|
|
|
this.resetUIAfterCompletion();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('处理完成事件失败:', error);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 仿真错误
|
|
|
|
|
this.eventSource.addEventListener('error', (event) => {
|
|
|
|
|
try {
|
|
|
|
|
const data = JSON.parse(event.data);
|
|
|
|
|
this.showError(`仿真错误: ${data.message}`);
|
|
|
|
|
|
|
|
|
|
// 重置UI
|
|
|
|
|
this.resetUIAfterCompletion();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('处理错误事件失败:', error);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 仿真终止
|
|
|
|
|
this.eventSource.addEventListener('terminated', (event) => {
|
|
|
|
|
try {
|
|
|
|
|
const data = JSON.parse(event.data);
|
|
|
|
|
this.showMessage(`仿真已终止: ${data.message}`);
|
|
|
|
|
|
|
|
|
|
// 在输出框中添加终止信息
|
|
|
|
|
const outputContent = this.shadowRoot.querySelector('#output-content');
|
|
|
|
|
outputContent.innerHTML += `\n\n仿真已终止:${data.message}`;
|
|
|
|
|
|
|
|
|
|
// 重置UI
|
|
|
|
|
this.resetUIAfterCompletion();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('处理终止事件失败:', error);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 仿真超时
|
|
|
|
|
this.eventSource.addEventListener('timeout', (event) => {
|
|
|
|
|
try {
|
|
|
|
|
const data = JSON.parse(event.data);
|
|
|
|
|
this.showError(`仿真超时: ${data.message}`);
|
|
|
|
|
|
|
|
|
|
// 在输出框中添加超时信息
|
|
|
|
|
const outputContent = this.shadowRoot.querySelector('#output-content');
|
|
|
|
|
outputContent.innerHTML += `\n\n仿真超时:${data.message}`;
|
|
|
|
|
|
|
|
|
|
// 重置UI
|
|
|
|
|
this.resetUIAfterCompletion();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('处理超时事件失败:', error);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-05-14 16:07:13 +08:00
|
|
|
|
// 修改连接错误处理
|
2025-04-28 12:25:20 +08:00
|
|
|
|
this.eventSource.onerror = (error) => {
|
|
|
|
|
console.error('SSE连接错误:', error);
|
|
|
|
|
|
2025-05-14 16:07:13 +08:00
|
|
|
|
// 检查连接状态
|
2025-04-28 12:25:20 +08:00
|
|
|
|
if (this.eventSource && this.eventSource.readyState === 2) { // CLOSED
|
2025-05-14 16:07:13 +08:00
|
|
|
|
// 如果还有重连次数,尝试重新连接
|
|
|
|
|
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
|
|
|
this.reconnectAttempts++;
|
|
|
|
|
this.showMessage(`连接断开,尝试重新连接 (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
|
|
|
|
|
setTimeout(() => this.checkAndConnectToExistingSimulation(), 1000);
|
|
|
|
|
} else {
|
|
|
|
|
this.showError('实时输出连接已断开');
|
|
|
|
|
this.resetUIAfterCompletion();
|
|
|
|
|
}
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 关闭SSE连接
|
|
|
|
|
closeEventSource() {
|
|
|
|
|
if (this.eventSource) {
|
2025-05-14 16:11:32 +08:00
|
|
|
|
// 通知后端清理tail进程
|
|
|
|
|
if (this.currentSimulationId) {
|
|
|
|
|
fetch(`/api/cleanup-simulation/${this.currentSimulationId}`, {
|
|
|
|
|
method: 'POST'
|
|
|
|
|
}).catch(error => {
|
|
|
|
|
console.error('清理仿真资源失败:', error);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-28 12:25:20 +08:00
|
|
|
|
this.eventSource.close();
|
|
|
|
|
this.eventSource = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 在仿真完成后重置UI
|
|
|
|
|
resetUIAfterCompletion() {
|
|
|
|
|
// 隐藏终止按钮
|
|
|
|
|
const stopButton = this.shadowRoot.querySelector('#stop-button');
|
|
|
|
|
if (stopButton) {
|
|
|
|
|
stopButton.style.display = 'none';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 重置运行按钮
|
|
|
|
|
const runButton = this.shadowRoot.querySelector('#run-button');
|
|
|
|
|
if (runButton) {
|
|
|
|
|
runButton.disabled = false;
|
|
|
|
|
runButton.textContent = '运行仿真';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 关闭SSE连接
|
|
|
|
|
this.closeEventSource();
|
|
|
|
|
|
2025-05-14 16:07:13 +08:00
|
|
|
|
// 清除当前仿真ID和重连计数
|
2025-04-28 12:25:20 +08:00
|
|
|
|
this.currentSimulationId = null;
|
2025-05-14 16:07:13 +08:00
|
|
|
|
this.reconnectAttempts = 0;
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async stopSimulation() {
|
|
|
|
|
if (!this.currentSimulationId) {
|
|
|
|
|
this.showMessage('没有正在运行的仿真');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
this.showMessage('正在终止仿真...');
|
|
|
|
|
|
|
|
|
|
const response = await fetch('/api/stop-simulation', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
id: this.currentSimulationId
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
this.showSuccess(`${result.message || '仿真已终止'}`);
|
|
|
|
|
|
|
|
|
|
// 终止信息会通过SSE推送到UI
|
|
|
|
|
// 此处不需要额外操作
|
|
|
|
|
} else {
|
|
|
|
|
const errorData = await response.json();
|
|
|
|
|
this.showError(`终止仿真失败: ${errorData.message || '未知错误'}`);
|
|
|
|
|
|
|
|
|
|
// 可能是SSE已断开,手动重置UI
|
|
|
|
|
this.resetUIAfterCompletion();
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('终止仿真失败:', error);
|
|
|
|
|
this.showError('终止仿真失败: ' + error.message);
|
|
|
|
|
|
|
|
|
|
// 手动重置UI
|
|
|
|
|
this.resetUIAfterCompletion();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
showError(message) {
|
|
|
|
|
const messageElement = this.shadowRoot.querySelector('#message');
|
|
|
|
|
messageElement.textContent = message;
|
|
|
|
|
messageElement.style.color = 'red';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
showMessage(message) {
|
|
|
|
|
const messageElement = this.shadowRoot.querySelector('#message');
|
|
|
|
|
messageElement.textContent = message;
|
|
|
|
|
messageElement.style.color = 'blue';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
showSuccess(message) {
|
|
|
|
|
const messageElement = this.shadowRoot.querySelector('#message');
|
|
|
|
|
messageElement.textContent = message;
|
|
|
|
|
messageElement.style.color = 'green';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
render() {
|
|
|
|
|
this.shadowRoot.innerHTML = `
|
|
|
|
|
<style>
|
|
|
|
|
:host {
|
|
|
|
|
display: block;
|
|
|
|
|
height: 100%;
|
|
|
|
|
overflow: auto;
|
|
|
|
|
padding: 16px;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.test-container {
|
|
|
|
|
background-color: white;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
|
|
|
padding: 16px;
|
|
|
|
|
height: 100%;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.config-selector {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.selector-label {
|
|
|
|
|
min-width: 120px;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
color: #333;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
flex: 1;
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
border: 1px solid #ccc;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
button {
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
background-color: #1976d2;
|
|
|
|
|
color: white;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
min-width: 100px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
button:hover {
|
|
|
|
|
background-color: #1565c0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
button:disabled {
|
|
|
|
|
background-color: #cccccc;
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#stop-button {
|
|
|
|
|
background-color: #d32f2f;
|
|
|
|
|
display: none;
|
|
|
|
|
margin-left: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#stop-button:hover {
|
|
|
|
|
background-color: #b71c1c;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#message {
|
|
|
|
|
margin: 10px 0;
|
|
|
|
|
padding: 10px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.content-container {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex: 1;
|
|
|
|
|
gap: 20px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.lists-container {
|
|
|
|
|
width: 250px;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 15px;
|
|
|
|
|
overflow: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.list-section {
|
|
|
|
|
border: 1px solid #e0e0e0;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.list-header {
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
padding: 10px;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
border-bottom: 1px solid #e0e0e0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.list-content {
|
|
|
|
|
overflow: auto;
|
|
|
|
|
padding: 10px;
|
|
|
|
|
max-height: 200px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.list-group {
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.list-group-header {
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
color: #333;
|
|
|
|
|
margin-bottom: 5px;
|
|
|
|
|
padding-bottom: 3px;
|
|
|
|
|
border-bottom: 1px solid #eee;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.list-items {
|
|
|
|
|
padding-left: 15px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.list-item {
|
|
|
|
|
padding: 5px 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.output-container {
|
|
|
|
|
flex: 1;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
border: 1px solid #e0e0e0;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.output-header {
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
padding: 10px;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
border-bottom: 1px solid #e0e0e0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.output-content {
|
|
|
|
|
flex: 1;
|
|
|
|
|
background-color: #2b2b2b;
|
|
|
|
|
color: #e0e0e0;
|
|
|
|
|
font-family: monospace;
|
|
|
|
|
padding: 10px;
|
|
|
|
|
overflow: auto;
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 终端颜色支持 */
|
|
|
|
|
.output-content .ansi-black { color: #000000; }
|
|
|
|
|
.output-content .ansi-red { color: #ff0000; }
|
|
|
|
|
.output-content .ansi-green { color: #00ff00; }
|
|
|
|
|
.output-content .ansi-yellow { color: #ffff00; }
|
|
|
|
|
.output-content .ansi-blue { color: #0000ff; }
|
|
|
|
|
.output-content .ansi-magenta { color: #ff00ff; }
|
|
|
|
|
.output-content .ansi-cyan { color: #00ffff; }
|
|
|
|
|
.output-content .ansi-white { color: #ffffff; }
|
|
|
|
|
|
|
|
|
|
.output-content .ansi-bright-black { color: #808080; }
|
|
|
|
|
.output-content .ansi-bright-red { color: #ff5555; }
|
|
|
|
|
.output-content .ansi-bright-green { color: #55ff55; }
|
|
|
|
|
.output-content .ansi-bright-yellow { color: #ffff55; }
|
|
|
|
|
.output-content .ansi-bright-blue { color: #5555ff; }
|
|
|
|
|
.output-content .ansi-bright-magenta { color: #ff55ff; }
|
|
|
|
|
.output-content .ansi-bright-cyan { color: #55ffff; }
|
|
|
|
|
.output-content .ansi-bright-white { color: #ffffff; }
|
|
|
|
|
|
|
|
|
|
.output-content .ansi-bg-black { background-color: #000000; }
|
|
|
|
|
.output-content .ansi-bg-red { background-color: #ff0000; }
|
|
|
|
|
.output-content .ansi-bg-green { background-color: #00ff00; }
|
|
|
|
|
.output-content .ansi-bg-yellow { background-color: #ffff00; }
|
|
|
|
|
.output-content .ansi-bg-blue { background-color: #0000ff; }
|
|
|
|
|
.output-content .ansi-bg-magenta { background-color: #ff00ff; }
|
|
|
|
|
.output-content .ansi-bg-cyan { background-color: #00ffff; }
|
|
|
|
|
.output-content .ansi-bg-white { background-color: #ffffff; }
|
|
|
|
|
|
|
|
|
|
.output-content .ansi-bold { font-weight: bold; }
|
|
|
|
|
.output-content .ansi-italic { font-style: italic; }
|
|
|
|
|
.output-content .ansi-underline { text-decoration: underline; }
|
|
|
|
|
|
|
|
|
|
.buttons-container {
|
|
|
|
|
display: flex;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
<div class="test-container">
|
|
|
|
|
<div class="config-selector">
|
|
|
|
|
<div class="selector-label">选择运行环境配置:</div>
|
|
|
|
|
<select id="scenario-select">
|
|
|
|
|
<option value="" disabled selected>-- 加载配置文件中... --</option>
|
|
|
|
|
</select>
|
|
|
|
|
<div class="buttons-container">
|
|
|
|
|
<button id="run-button">运行仿真</button>
|
|
|
|
|
<button id="stop-button">终止仿真</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="message"></div>
|
|
|
|
|
<div class="content-container">
|
|
|
|
|
<div class="lists-container">
|
|
|
|
|
<div class="list-section">
|
|
|
|
|
<div class="list-header">模型列表</div>
|
|
|
|
|
<div id="model-list" class="list-content"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="list-section">
|
|
|
|
|
<div class="list-header">服务列表</div>
|
|
|
|
|
<div id="service-list" class="list-content"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="output-container">
|
|
|
|
|
<div class="output-header">运行输出</div>
|
|
|
|
|
<div id="output-content" class="output-content">等待仿真运行...</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
// 添加事件监听器
|
|
|
|
|
const runButton = this.shadowRoot.querySelector('#run-button');
|
|
|
|
|
runButton.addEventListener('click', () => this.runTest());
|
|
|
|
|
|
|
|
|
|
const stopButton = this.shadowRoot.querySelector('#stop-button');
|
|
|
|
|
stopButton.addEventListener('click', () => this.stopSimulation());
|
|
|
|
|
|
|
|
|
|
const scenarioSelect = this.shadowRoot.querySelector('#scenario-select');
|
|
|
|
|
scenarioSelect.addEventListener('change', (event) => this.onScenarioSelected(event));
|
|
|
|
|
}
|
2025-05-14 16:07:13 +08:00
|
|
|
|
|
|
|
|
|
// 添加新方法:检查并连接到已有的仿真
|
|
|
|
|
async checkAndConnectToExistingSimulation() {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch('/api/check-xnengine');
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
if (data.running) {
|
|
|
|
|
this.showMessage('检测到正在运行的仿真,正在重新连接...');
|
|
|
|
|
|
|
|
|
|
// 更新UI以反映运行状态
|
|
|
|
|
const runButton = this.shadowRoot.querySelector('#run-button');
|
|
|
|
|
if (runButton) {
|
|
|
|
|
runButton.disabled = true;
|
|
|
|
|
runButton.textContent = '运行中...';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 显示终止按钮
|
|
|
|
|
const stopButton = this.shadowRoot.querySelector('#stop-button');
|
|
|
|
|
if (stopButton) {
|
|
|
|
|
stopButton.style.display = 'block';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 使用进程ID作为仿真ID重新连接
|
|
|
|
|
this.currentSimulationId = data.pid.toString();
|
|
|
|
|
|
|
|
|
|
// 确保场景文件列表已加载
|
|
|
|
|
if (this.scenarioFiles.length === 0) {
|
|
|
|
|
await this.fetchScenarioFiles();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 如果有配置文件路径,自动选择并加载内容
|
|
|
|
|
if (data.scenarioFile) {
|
|
|
|
|
const scenarioSelect = this.shadowRoot.querySelector('#scenario-select');
|
|
|
|
|
if (scenarioSelect) {
|
|
|
|
|
// 检查配置文件是否在列表中
|
|
|
|
|
const fileExists = Array.from(scenarioSelect.options).some(opt => opt.value === data.scenarioFile);
|
|
|
|
|
if (fileExists) {
|
|
|
|
|
// 先设置当前场景
|
|
|
|
|
this.currentScenario = data.scenarioFile;
|
|
|
|
|
|
|
|
|
|
// 加载配置文件内容
|
|
|
|
|
try {
|
|
|
|
|
this.showMessage('加载配置文件内容...');
|
|
|
|
|
const response = await fetch(`/api/file-content?path=${encodeURIComponent(data.scenarioFile)}`);
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error('无法获取配置文件内容');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const xmlContent = await response.text();
|
|
|
|
|
this.parseScenarioXML(xmlContent);
|
|
|
|
|
|
|
|
|
|
// 更新下拉框选择
|
|
|
|
|
scenarioSelect.value = data.scenarioFile;
|
|
|
|
|
|
|
|
|
|
// 更新模型和服务列表
|
|
|
|
|
this.updateModelAndServiceLists();
|
|
|
|
|
this.showMessage('配置文件加载完成');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('加载配置文件失败:', error);
|
|
|
|
|
this.showError('加载配置文件失败: ' + error.message);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
console.warn('配置文件不在列表中:', data.scenarioFile);
|
|
|
|
|
this.showError('配置文件不在列表中: ' + data.scenarioFile);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 清空并初始化输出框
|
|
|
|
|
const outputContent = this.shadowRoot.querySelector('#output-content');
|
|
|
|
|
outputContent.innerHTML = '重新连接到运行中的仿真...\n';
|
|
|
|
|
|
|
|
|
|
// 连接到SSE获取输出
|
|
|
|
|
this.connectToEventSource(this.currentSimulationId);
|
|
|
|
|
|
|
|
|
|
// 更新状态为已连接
|
|
|
|
|
this.showSuccess('已连接到运行中的仿真');
|
|
|
|
|
|
|
|
|
|
// 重置重连尝试次数
|
|
|
|
|
this.reconnectAttempts = 0;
|
|
|
|
|
} else {
|
|
|
|
|
// 如果没有运行中的仿真,重置UI
|
|
|
|
|
this.resetUIAfterCompletion();
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('检查XNEngine进程失败:', error);
|
|
|
|
|
this.showError('检查仿真状态失败: ' + error.message);
|
|
|
|
|
this.resetUIAfterCompletion();
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-05-14 16:11:32 +08:00
|
|
|
|
|
|
|
|
|
// 添加组件销毁时的清理方法
|
|
|
|
|
disconnectedCallback() {
|
|
|
|
|
// 清理SSE连接和相关资源
|
|
|
|
|
this.closeEventSource();
|
|
|
|
|
|
|
|
|
|
// 重置所有状态
|
|
|
|
|
this.currentSimulationId = null;
|
|
|
|
|
this.reconnectAttempts = 0;
|
|
|
|
|
this.scenarioFiles = [];
|
|
|
|
|
this.currentScenario = null;
|
|
|
|
|
this.modelGroups = [];
|
|
|
|
|
this.services = [];
|
|
|
|
|
}
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
customElements.define('run-simulation', RunSimulation);
|