XNSim/XNSimHtml/components/run-simulation.js

1080 lines
41 KiB
JavaScript
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.

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;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 3;
}
// 添加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;
}
}
}
// 检查是否有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();
}
}
// 检查仿真状态
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() {
// 先加载场景文件列表
this.fetchScenarioFiles();
// 然后渲染UI
this.render();
// 最后检查是否有XNEngine进程在运行
this.checkAndConnectToExistingSimulation();
}
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
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();
// 准备启动参数
const simulationArgs = [selectedScenario];
// 调用后端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;
// 如果是连接到现有进程,显示相应消息
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(`仿真已启动`);
}
// 设置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);
console.log('收到状态事件:', data);
if (data.running === false) {
// 仿真已经不存在或已结束
this.showMessage(data.message || '仿真不存在或已结束');
// 如果是进程不存在,尝试重新连接
if (data.message === '仿真不存在或已结束' && this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
this.showMessage(`尝试重新连接 (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
setTimeout(() => this.checkAndConnectToExistingSimulation(), 1000);
return;
}
// 重置UI
this.resetUIAfterCompletion();
} else if (data.running === true) {
// 更新状态为已连接
this.showSuccess('已连接到运行中的仿真');
}
} catch (error) {
console.error('处理状态事件失败:', error);
}
});
// 仿真完成
this.eventSource.addEventListener('completed', (event) => {
try {
const data = JSON.parse(event.data);
console.log('收到完成事件:', 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);
console.log('收到错误事件:', 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);
console.log('收到终止事件:', 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);
console.log('收到超时事件:', 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);
}
});
// 修改连接错误处理
this.eventSource.onerror = (error) => {
console.error('SSE连接错误:', error);
// 检查连接状态
if (this.eventSource && this.eventSource.readyState === 2) { // CLOSED
// 如果还有重连次数,尝试重新连接
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
this.showMessage(`连接断开,尝试重新连接 (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
setTimeout(() => this.checkAndConnectToExistingSimulation(), 1000);
} else {
this.showError('实时输出连接已断开');
this.resetUIAfterCompletion();
}
}
};
}
// 关闭SSE连接
closeEventSource() {
if (this.eventSource) {
// 通知后端清理tail进程
if (this.currentSimulationId) {
fetch(`/api/cleanup-simulation/${this.currentSimulationId}`, {
method: 'POST'
}).catch(error => {
console.error('清理仿真资源失败:', error);
});
}
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();
// 清除当前仿真ID和重连计数
this.currentSimulationId = null;
this.reconnectAttempts = 0;
}
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));
}
// 添加新方法:检查并连接到已有的仿真
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();
}
}
// 添加组件销毁时的清理方法
disconnectedCallback() {
// 清理SSE连接和相关资源
this.closeEventSource();
// 重置所有状态
this.currentSimulationId = null;
this.reconnectAttempts = 0;
this.scenarioFiles = [];
this.currentScenario = null;
this.modelGroups = [];
this.services = [];
}
}
customElements.define('run-simulation', RunSimulation);