1080 lines
41 KiB
JavaScript
1080 lines
41 KiB
JavaScript
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, '&')
|
||
.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();
|
||
|
||
// 准备启动参数
|
||
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);
|