XNSim/XNSimHtml/components/run-simulation.js

1075 lines
41 KiB
JavaScript
Raw Normal View History

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;
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;
}
}
}
// 检查是否有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-04-28 12:25:20 +08:00
this.fetchScenarioFiles();
// 然后渲染UI
2025-04-28 12:25:20 +08:00
this.render();
// 最后检查是否有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, '&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 = ['-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;
// 如果是连接到现有进程,显示相应消息
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 || '仿真不存在或已结束');
// 如果是进程不存在,尝试重新连接
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();
} 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-04-28 12:25:20 +08:00
this.eventSource.onerror = (error) => {
console.error('SSE连接错误:', error);
// 检查连接状态
2025-04-28 12:25:20 +08:00
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();
}
2025-04-28 12:25:20 +08:00
}
};
}
// 关闭SSE连接
closeEventSource() {
if (this.eventSource) {
// 通知后端清理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();
// 清除当前仿真ID和重连计数
2025-04-28 12:25:20 +08:00
this.currentSimulationId = null;
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));
}
// 添加新方法:检查并连接到已有的仿真
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 = [];
}
2025-04-28 12:25:20 +08:00
}
customElements.define('run-simulation', RunSimulation);