2025-05-29 10:01:36 +08:00
|
|
|
|
class RunSim extends HTMLElement {
|
2025-04-28 12:25:20 +08:00
|
|
|
|
constructor() {
|
|
|
|
|
super();
|
|
|
|
|
this.attachShadow({ mode: 'open' });
|
|
|
|
|
this.modelGroups = [];
|
|
|
|
|
this.services = [];
|
|
|
|
|
this.eventSource = null;
|
2025-05-29 09:26:44 +08:00
|
|
|
|
this.logFileWatcher = null;
|
|
|
|
|
this.logFilePollingInterval = null;
|
2025-05-29 10:01:36 +08:00
|
|
|
|
this.currentSimulationId = null;
|
|
|
|
|
this.reconnectAttempts = 0;
|
|
|
|
|
this.maxReconnectAttempts = 3;
|
2025-05-29 14:38:09 +08:00
|
|
|
|
this.isPaused = false;
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
connectedCallback() {
|
|
|
|
|
this.render();
|
2025-05-29 09:26:44 +08:00
|
|
|
|
this.loadModelGroups();
|
|
|
|
|
this.loadServices();
|
2025-05-29 10:01:36 +08:00
|
|
|
|
// 检查并连接到已有的仿真
|
|
|
|
|
this.checkAndConnectToExistingSimulation();
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-29 09:26:44 +08:00
|
|
|
|
async loadModelGroups() {
|
2025-04-28 12:25:20 +08:00
|
|
|
|
try {
|
2025-05-29 09:26:44 +08:00
|
|
|
|
const savedSelection = localStorage.getItem('xnsim-selection');
|
|
|
|
|
const selection = savedSelection ? JSON.parse(savedSelection) : {};
|
|
|
|
|
const confID = selection.configurationId;
|
|
|
|
|
|
|
|
|
|
if (!confID) {
|
|
|
|
|
this.showError('未选择构型');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取模型组列表
|
|
|
|
|
const response = await fetch(`/api/configurations/${confID}/model-groups`);
|
2025-04-28 12:25:20 +08:00
|
|
|
|
if (!response.ok) {
|
2025-05-29 09:26:44 +08:00
|
|
|
|
throw new Error('获取模型组失败');
|
|
|
|
|
}
|
|
|
|
|
const groups = await response.json();
|
|
|
|
|
|
|
|
|
|
// 获取每个模型组下的模型
|
|
|
|
|
this.modelGroups = [];
|
|
|
|
|
for (const group of groups) {
|
|
|
|
|
const modelsResponse = await fetch(`/api/model-groups/${group.GroupID}/models`);
|
|
|
|
|
if (!modelsResponse.ok) {
|
|
|
|
|
throw new Error(`获取模型组 ${group.GroupName} 的模型失败`);
|
|
|
|
|
}
|
|
|
|
|
const models = await modelsResponse.json();
|
|
|
|
|
|
|
|
|
|
this.modelGroups.push({
|
|
|
|
|
name: group.GroupName,
|
|
|
|
|
groupId: group.GroupID,
|
|
|
|
|
freq: group.Freq,
|
|
|
|
|
priority: group.Priority,
|
|
|
|
|
cpuAff: group.CPUAff,
|
|
|
|
|
models: models.map(model => ({
|
|
|
|
|
className: model.ClassName,
|
|
|
|
|
version: model.ModelVersion
|
|
|
|
|
}))
|
|
|
|
|
});
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
2025-05-29 09:26:44 +08:00
|
|
|
|
|
|
|
|
|
this.updateModelList();
|
2025-04-28 12:25:20 +08:00
|
|
|
|
} catch (error) {
|
2025-05-29 09:26:44 +08:00
|
|
|
|
console.error('加载模型组失败:', error);
|
|
|
|
|
this.showError('加载模型组失败: ' + error.message);
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-29 09:26:44 +08:00
|
|
|
|
async loadServices() {
|
|
|
|
|
try {
|
|
|
|
|
const savedSelection = localStorage.getItem('xnsim-selection');
|
|
|
|
|
const selection = savedSelection ? JSON.parse(savedSelection) : {};
|
|
|
|
|
const confID = selection.configurationId;
|
2025-04-28 12:25:20 +08:00
|
|
|
|
|
2025-05-29 09:26:44 +08:00
|
|
|
|
if (!confID) {
|
|
|
|
|
this.showError('未选择构型');
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-04-28 12:25:20 +08:00
|
|
|
|
|
2025-05-29 09:26:44 +08:00
|
|
|
|
// 获取服务列表
|
|
|
|
|
const response = await fetch(`/api/configurations/${confID}/services`);
|
2025-04-28 12:25:20 +08:00
|
|
|
|
if (!response.ok) {
|
2025-05-29 09:26:44 +08:00
|
|
|
|
throw new Error('获取服务列表失败');
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
2025-05-29 09:26:44 +08:00
|
|
|
|
const services = await response.json();
|
|
|
|
|
|
|
|
|
|
this.services = services.map(service => ({
|
|
|
|
|
className: service.ClassName,
|
|
|
|
|
version: service.ServiceVersion
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
this.updateServiceList();
|
2025-04-28 12:25:20 +08:00
|
|
|
|
} catch (error) {
|
2025-05-29 09:26:44 +08:00
|
|
|
|
console.error('加载服务失败:', error);
|
|
|
|
|
this.showError('加载服务失败: ' + error.message);
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2025-05-29 09:26:44 +08:00
|
|
|
|
|
|
|
|
|
updateModelList() {
|
2025-04-28 12:25:20 +08:00
|
|
|
|
const modelListContainer = this.shadowRoot.querySelector('#model-list');
|
2025-05-29 09:26:44 +08:00
|
|
|
|
if (!modelListContainer) return;
|
|
|
|
|
|
2025-04-28 12:25:20 +08:00
|
|
|
|
modelListContainer.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
this.modelGroups.forEach(group => {
|
|
|
|
|
const groupItem = document.createElement('div');
|
|
|
|
|
groupItem.className = 'list-group';
|
|
|
|
|
|
2025-05-29 09:26:44 +08:00
|
|
|
|
// 组名
|
2025-04-28 12:25:20 +08:00
|
|
|
|
const groupHeader = document.createElement('div');
|
|
|
|
|
groupHeader.className = 'list-group-header';
|
|
|
|
|
groupHeader.textContent = group.name;
|
|
|
|
|
groupItem.appendChild(groupHeader);
|
2025-05-29 09:26:44 +08:00
|
|
|
|
|
|
|
|
|
// 组信息(以小字显示)
|
|
|
|
|
const groupInfo = document.createElement('div');
|
|
|
|
|
groupInfo.className = 'list-group-info';
|
|
|
|
|
groupInfo.textContent = `频率:${group.freq}.0 Hz / 优先级:${group.priority} / 亲和性:${group.cpuAff}`;
|
|
|
|
|
groupItem.appendChild(groupInfo);
|
2025-04-28 12:25:20 +08:00
|
|
|
|
|
|
|
|
|
const modelsList = document.createElement('div');
|
|
|
|
|
modelsList.className = 'list-items';
|
|
|
|
|
|
|
|
|
|
group.models.forEach(model => {
|
|
|
|
|
const modelItem = document.createElement('div');
|
|
|
|
|
modelItem.className = 'list-item';
|
2025-05-29 09:26:44 +08:00
|
|
|
|
modelItem.textContent = `${model.className} (v${model.version})`;
|
2025-04-28 12:25:20 +08:00
|
|
|
|
modelsList.appendChild(modelItem);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
groupItem.appendChild(modelsList);
|
|
|
|
|
modelListContainer.appendChild(groupItem);
|
|
|
|
|
});
|
2025-05-29 09:26:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateServiceList() {
|
2025-04-28 12:25:20 +08:00
|
|
|
|
const serviceListContainer = this.shadowRoot.querySelector('#service-list');
|
2025-05-29 09:26:44 +08:00
|
|
|
|
if (!serviceListContainer) return;
|
|
|
|
|
|
2025-04-28 12:25:20 +08:00
|
|
|
|
serviceListContainer.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
this.services.forEach(service => {
|
|
|
|
|
const serviceItem = document.createElement('div');
|
|
|
|
|
serviceItem.className = 'list-item';
|
2025-05-29 09:26:44 +08:00
|
|
|
|
serviceItem.textContent = `${service.className} (v${service.version})`;
|
2025-04-28 12:25:20 +08:00
|
|
|
|
serviceListContainer.appendChild(serviceItem);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async runTest() {
|
2025-05-29 09:26:44 +08:00
|
|
|
|
const savedSelection = localStorage.getItem('xnsim-selection');
|
|
|
|
|
const selection = savedSelection ? JSON.parse(savedSelection) : {};
|
|
|
|
|
const confID = selection.configurationId;
|
2025-04-28 12:25:20 +08:00
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
this.showMessage('准备运行测试...');
|
|
|
|
|
const runButton = this.shadowRoot.querySelector('#run-button');
|
|
|
|
|
runButton.disabled = true;
|
|
|
|
|
runButton.textContent = '运行中...';
|
|
|
|
|
|
|
|
|
|
// 清空并初始化输出框
|
|
|
|
|
const outputContent = this.shadowRoot.querySelector('#output-content');
|
|
|
|
|
outputContent.innerHTML = '开始执行测试...\n';
|
|
|
|
|
|
|
|
|
|
// 准备启动参数
|
2025-05-29 09:26:44 +08:00
|
|
|
|
const simulationArgs = ['-id', confID, '-test'];
|
2025-04-28 12:25:20 +08:00
|
|
|
|
|
2025-05-26 17:01:23 +08:00
|
|
|
|
// 调用后端API执行测试
|
2025-04-28 12:25:20 +08:00
|
|
|
|
const response = await fetch('/api/run-simulation', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
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();
|
|
|
|
|
|
2025-05-29 09:26:44 +08:00
|
|
|
|
// 获取进程ID和日志文件路径
|
|
|
|
|
const processId = responseData.simulationId;
|
|
|
|
|
const logFile = responseData.logFile;
|
|
|
|
|
|
|
|
|
|
// 开始轮询日志文件
|
|
|
|
|
this.startLogFilePolling(logFile);
|
2025-04-28 12:25:20 +08:00
|
|
|
|
|
2025-05-26 17:01:23 +08:00
|
|
|
|
// 根据测试结果更新UI
|
|
|
|
|
if (responseData.success) {
|
|
|
|
|
this.showSuccess('测试已启动');
|
|
|
|
|
} else {
|
|
|
|
|
this.showError(`测试启动失败: ${responseData.message || '未知错误'}`);
|
|
|
|
|
}
|
2025-04-28 12:25:20 +08:00
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('执行测试失败:', error);
|
|
|
|
|
this.showError('执行测试失败: ' + error.message);
|
|
|
|
|
|
|
|
|
|
// 显示错误详情
|
|
|
|
|
const outputContent = this.shadowRoot.querySelector('#output-content');
|
|
|
|
|
outputContent.innerHTML += `\n\n执行错误: ${error.message}`;
|
2025-05-26 17:01:23 +08:00
|
|
|
|
} finally {
|
2025-04-28 12:25:20 +08:00
|
|
|
|
// 重置UI
|
2025-05-26 17:01:23 +08:00
|
|
|
|
const runButton = this.shadowRoot.querySelector('#run-button');
|
|
|
|
|
runButton.disabled = false;
|
|
|
|
|
runButton.textContent = '运行测试';
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2025-05-26 17:01:23 +08:00
|
|
|
|
|
2025-05-29 09:26:44 +08:00
|
|
|
|
// 开始轮询日志文件
|
|
|
|
|
startLogFilePolling(logFile) {
|
|
|
|
|
// 清除之前的轮询
|
|
|
|
|
if (this.logFilePollingInterval) {
|
|
|
|
|
clearInterval(this.logFilePollingInterval);
|
|
|
|
|
this.logFilePollingInterval = null;
|
2025-05-26 17:01:23 +08:00
|
|
|
|
}
|
2025-05-29 09:26:44 +08:00
|
|
|
|
|
|
|
|
|
let lastPosition = 0;
|
|
|
|
|
const outputContent = this.shadowRoot.querySelector('#output-content');
|
|
|
|
|
|
|
|
|
|
// 每100ms检查一次文件
|
|
|
|
|
this.logFilePollingInterval = setInterval(async () => {
|
2025-04-28 12:25:20 +08:00
|
|
|
|
try {
|
2025-05-29 09:26:44 +08:00
|
|
|
|
// 检查进程是否还在运行
|
|
|
|
|
const response = await fetch(`/api/check-process/${logFile.split('_')[1].split('.')[0]}`);
|
|
|
|
|
const data = await response.json();
|
2025-04-28 12:25:20 +08:00
|
|
|
|
|
2025-05-29 09:26:44 +08:00
|
|
|
|
if (!data.running) {
|
|
|
|
|
// 进程已结束,读取剩余内容并停止轮询
|
|
|
|
|
const finalContent = await this.readLogFile(logFile, lastPosition);
|
|
|
|
|
if (finalContent) {
|
|
|
|
|
outputContent.innerHTML += this.convertAnsiToHtml(finalContent);
|
|
|
|
|
outputContent.scrollTop = outputContent.scrollHeight;
|
|
|
|
|
}
|
2025-04-28 12:25:20 +08:00
|
|
|
|
|
2025-05-29 09:26:44 +08:00
|
|
|
|
clearInterval(this.logFilePollingInterval);
|
|
|
|
|
this.logFilePollingInterval = null;
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
this.showSuccess('测试执行成功');
|
|
|
|
|
} else {
|
|
|
|
|
this.showError(`测试执行失败: ${data.message || '未知错误'}`);
|
|
|
|
|
}
|
|
|
|
|
return;
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
2025-05-29 09:26:44 +08:00
|
|
|
|
|
|
|
|
|
// 读取新的日志内容
|
|
|
|
|
const content = await this.readLogFile(logFile, lastPosition);
|
|
|
|
|
if (content) {
|
|
|
|
|
outputContent.innerHTML += this.convertAnsiToHtml(content);
|
|
|
|
|
outputContent.scrollTop = outputContent.scrollHeight;
|
|
|
|
|
lastPosition += content.length;
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2025-05-29 09:26:44 +08:00
|
|
|
|
console.error('读取日志文件失败:', error);
|
|
|
|
|
clearInterval(this.logFilePollingInterval);
|
|
|
|
|
this.logFilePollingInterval = null;
|
|
|
|
|
this.showError('读取日志文件失败: ' + error.message);
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
2025-05-29 09:26:44 +08:00
|
|
|
|
}, 100);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 读取日志文件
|
|
|
|
|
async readLogFile(logFile, startPosition) {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`/api/read-log-file?file=${encodeURIComponent(logFile)}&position=${startPosition}`);
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error('读取日志文件失败');
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
2025-05-29 09:26:44 +08:00
|
|
|
|
const data = await response.json();
|
|
|
|
|
return data.content;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('读取日志文件失败:', error);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
2025-05-29 09:26:44 +08:00
|
|
|
|
|
2025-05-29 10:01:36 +08:00
|
|
|
|
// 连接到SSE事件源获取实时输出
|
2025-05-29 14:38:09 +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-simulation-button');
|
|
|
|
|
const pauseButton = this.shadowRoot.querySelector('#pause-simulation-button');
|
|
|
|
|
const stopButton = this.shadowRoot.querySelector('#stop-simulation-button');
|
|
|
|
|
|
|
|
|
|
if (runButton) {
|
|
|
|
|
runButton.disabled = true;
|
|
|
|
|
runButton.textContent = '运行中...';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (pauseButton) {
|
|
|
|
|
pauseButton.disabled = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (stopButton) {
|
|
|
|
|
stopButton.disabled = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 使用进程ID作为仿真ID
|
|
|
|
|
this.currentSimulationId = data.pid.toString();
|
|
|
|
|
|
|
|
|
|
// 清空并初始化输出框
|
|
|
|
|
const outputContent = this.shadowRoot.querySelector('#output-content');
|
|
|
|
|
outputContent.innerHTML = '重新连接到运行中的仿真...\n';
|
|
|
|
|
|
|
|
|
|
// 检查现有SSE连接是否有效
|
|
|
|
|
if (!this.eventSource || this.eventSource.readyState === 2) { // 2表示CLOSED
|
|
|
|
|
// 只有在没有有效连接时才创建新连接
|
|
|
|
|
this.connectToEventSource(this.currentSimulationId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查引擎的暂停状态
|
|
|
|
|
pauseButton.textContent = this.isPaused ? '继续仿真' : '暂停仿真';
|
|
|
|
|
|
|
|
|
|
// 更新状态为已连接
|
|
|
|
|
this.showSuccess('已连接到运行中的仿真');
|
|
|
|
|
|
|
|
|
|
// 重置重连尝试次数
|
|
|
|
|
this.reconnectAttempts = 0;
|
|
|
|
|
} else {
|
|
|
|
|
// 如果没有运行中的仿真,重置UI
|
|
|
|
|
this.resetSimulationButtons();
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('检查XNEngine进程失败:', error);
|
|
|
|
|
this.showError('检查仿真状态失败: ' + error.message);
|
|
|
|
|
this.resetSimulationButtons();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-29 10:01:36 +08:00
|
|
|
|
connectToEventSource(simulationId) {
|
2025-05-29 14:38:09 +08:00
|
|
|
|
// 只有在没有有效连接时才创建新连接
|
|
|
|
|
if (this.eventSource && this.eventSource.readyState !== 2) { // 2表示CLOSED
|
|
|
|
|
return; // 如果连接有效,直接返回
|
2025-05-29 10:01:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 创建新的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.checkSimulationStatus(simulationId), 1000);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 重置UI
|
|
|
|
|
this.resetSimulationButtons();
|
|
|
|
|
} else if (data.running === true) {
|
|
|
|
|
// 更新状态为已连接
|
|
|
|
|
this.showSuccess('已连接到运行中的仿真');
|
|
|
|
|
}
|
|
|
|
|
} 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.resetSimulationButtons();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('处理完成事件失败:', error);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 仿真错误
|
|
|
|
|
this.eventSource.addEventListener('error', (event) => {
|
|
|
|
|
try {
|
|
|
|
|
const data = JSON.parse(event.data);
|
|
|
|
|
this.showError(`仿真错误: ${data.message}`);
|
|
|
|
|
|
|
|
|
|
// 重置UI
|
|
|
|
|
this.resetSimulationButtons();
|
|
|
|
|
} 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.resetSimulationButtons();
|
|
|
|
|
} 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.checkSimulationStatus(this.currentSimulationId), 1000);
|
|
|
|
|
} else {
|
|
|
|
|
this.showError('实时输出连接已断开');
|
|
|
|
|
this.resetSimulationButtons();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查仿真状态
|
|
|
|
|
async checkSimulationStatus(simulationId) {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`/api/check-process/${simulationId}`);
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
if (data.running) {
|
|
|
|
|
// 仿真仍在运行,重新连接到事件源
|
|
|
|
|
this.showMessage('重新连接到运行中的仿真...');
|
|
|
|
|
this.connectToEventSource(simulationId);
|
|
|
|
|
} else {
|
|
|
|
|
// 仿真已经停止,重置UI
|
|
|
|
|
this.showMessage('仿真已结束');
|
|
|
|
|
this.resetSimulationButtons();
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('检查仿真状态失败:', error);
|
|
|
|
|
// 假设仿真已停止
|
|
|
|
|
this.resetSimulationButtons();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async runSimulation() {
|
|
|
|
|
const savedSelection = localStorage.getItem('xnsim-selection');
|
|
|
|
|
const selection = savedSelection ? JSON.parse(savedSelection) : {};
|
|
|
|
|
const confID = selection.configurationId;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
this.showMessage('准备运行仿真...');
|
|
|
|
|
const runButton = this.shadowRoot.querySelector('#run-simulation-button');
|
|
|
|
|
const pauseButton = this.shadowRoot.querySelector('#pause-simulation-button');
|
|
|
|
|
const stopButton = this.shadowRoot.querySelector('#stop-simulation-button');
|
|
|
|
|
|
|
|
|
|
runButton.disabled = true;
|
|
|
|
|
pauseButton.disabled = false;
|
|
|
|
|
stopButton.disabled = false;
|
|
|
|
|
runButton.textContent = '运行中...';
|
|
|
|
|
|
|
|
|
|
// 清空并初始化输出框
|
|
|
|
|
const outputContent = this.shadowRoot.querySelector('#output-content');
|
|
|
|
|
outputContent.innerHTML = '开始执行仿真...\n';
|
|
|
|
|
|
2025-05-29 14:38:09 +08:00
|
|
|
|
// 获取构型参数
|
|
|
|
|
const configResponse = await fetch(`/api/configurations/${confID}`);
|
|
|
|
|
if (!configResponse.ok) {
|
|
|
|
|
throw new Error('获取构型参数失败');
|
|
|
|
|
}
|
|
|
|
|
const configData = await configResponse.json();
|
|
|
|
|
|
|
|
|
|
// 从构型参数中提取域ID
|
|
|
|
|
const domainId = configData.DomainID;
|
|
|
|
|
if (!domainId) {
|
|
|
|
|
throw new Error('构型参数中未找到有效的域ID');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 生成唯一的监控器ID
|
|
|
|
|
const monitorId = `sim_${Date.now()}`;
|
|
|
|
|
|
|
|
|
|
// 初始化DDS监控
|
|
|
|
|
const ddsInitResponse = await fetch('/api/dds-monitor/initialize', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({ domainId, monitorId })
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!ddsInitResponse.ok) {
|
|
|
|
|
throw new Error('初始化DDS监控失败');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ddsInitResult = await ddsInitResponse.json();
|
|
|
|
|
if (ddsInitResult.error) {
|
|
|
|
|
throw new Error(ddsInitResult.error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 初始化引擎控制
|
|
|
|
|
const initResponse = await fetch('/api/system-control/initialize', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!initResponse.ok) {
|
|
|
|
|
throw new Error('初始化引擎控制失败');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const initResult = await initResponse.json();
|
|
|
|
|
if (!initResult.success) {
|
|
|
|
|
throw new Error(initResult.message || '初始化引擎控制失败');
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-29 10:01:36 +08:00
|
|
|
|
// 准备启动参数
|
|
|
|
|
const simulationArgs = ['-id', confID];
|
|
|
|
|
|
|
|
|
|
// 调用后端API执行仿真
|
|
|
|
|
const response = await fetch('/api/run-simulation', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
args: simulationArgs
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
// 保存当前仿真ID
|
|
|
|
|
this.currentSimulationId = simulationId;
|
|
|
|
|
|
|
|
|
|
// 建立SSE连接
|
|
|
|
|
this.connectToEventSource(simulationId);
|
|
|
|
|
|
|
|
|
|
// 根据启动结果更新UI
|
|
|
|
|
if (responseData.success) {
|
|
|
|
|
this.showSuccess('仿真已启动');
|
|
|
|
|
} else {
|
|
|
|
|
this.showError(`仿真启动失败: ${responseData.message || '未知错误'}`);
|
|
|
|
|
this.resetSimulationButtons();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('执行仿真失败:', error);
|
|
|
|
|
this.showError('执行仿真失败: ' + error.message);
|
|
|
|
|
|
|
|
|
|
// 重置按钮状态
|
|
|
|
|
this.resetSimulationButtons();
|
|
|
|
|
|
|
|
|
|
// 显示错误详情
|
|
|
|
|
const outputContent = this.shadowRoot.querySelector('#output-content');
|
|
|
|
|
outputContent.innerHTML += `\n\n执行错误: ${error.message}`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 添加reactivate方法,用于从缓存中恢复时检查仿真状态
|
|
|
|
|
async reactivate() {
|
|
|
|
|
// 检查是否有XNEngine进程在运行
|
|
|
|
|
await this.checkAndConnectToExistingSimulation();
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-29 09:26:44 +08:00
|
|
|
|
// 在组件销毁时清理资源
|
|
|
|
|
disconnectedCallback() {
|
|
|
|
|
if (this.logFilePollingInterval) {
|
|
|
|
|
clearInterval(this.logFilePollingInterval);
|
|
|
|
|
this.logFilePollingInterval = null;
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
2025-05-29 10:01:36 +08:00
|
|
|
|
if (this.eventSource) {
|
|
|
|
|
this.eventSource.close();
|
|
|
|
|
this.eventSource = null;
|
|
|
|
|
}
|
|
|
|
|
this.currentSimulationId = null;
|
|
|
|
|
this.reconnectAttempts = 0;
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.content-container {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex: 1;
|
|
|
|
|
gap: 20px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.lists-container {
|
2025-05-29 09:26:44 +08:00
|
|
|
|
width: 320px;
|
2025-04-28 12:25:20 +08:00
|
|
|
|
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;
|
2025-05-29 09:26:44 +08:00
|
|
|
|
margin-bottom: 2px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.list-group-info {
|
|
|
|
|
font-size: 0.85em;
|
|
|
|
|
color: #666;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
padding-left: 2px;
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.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;
|
2025-05-29 09:26:44 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#message {
|
|
|
|
|
flex: 1;
|
|
|
|
|
text-align: center;
|
|
|
|
|
margin: 0 10px;
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.output-content {
|
|
|
|
|
flex: 1;
|
|
|
|
|
background-color: #2b2b2b;
|
|
|
|
|
color: #e0e0e0;
|
|
|
|
|
font-family: monospace;
|
|
|
|
|
padding: 10px;
|
|
|
|
|
overflow: auto;
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
}
|
2025-05-29 09:26:44 +08:00
|
|
|
|
|
|
|
|
|
/* ANSI颜色样式 */
|
|
|
|
|
.ansi-black { color: #000000; }
|
|
|
|
|
.ansi-red { color: #ff0000; }
|
|
|
|
|
.ansi-green { color: #00ff00; }
|
|
|
|
|
.ansi-yellow { color: #ffff00; }
|
|
|
|
|
.ansi-blue { color: #0000ff; }
|
|
|
|
|
.ansi-magenta { color: #ff00ff; }
|
|
|
|
|
.ansi-cyan { color: #00ffff; }
|
|
|
|
|
.ansi-white { color: #ffffff; }
|
2025-04-28 12:25:20 +08:00
|
|
|
|
|
2025-05-29 09:26:44 +08:00
|
|
|
|
.ansi-bright-black { color: #666666; }
|
|
|
|
|
.ansi-bright-red { color: #ff6666; }
|
|
|
|
|
.ansi-bright-green { color: #66ff66; }
|
|
|
|
|
.ansi-bright-yellow { color: #ffff66; }
|
|
|
|
|
.ansi-bright-blue { color: #6666ff; }
|
|
|
|
|
.ansi-bright-magenta { color: #ff66ff; }
|
|
|
|
|
.ansi-bright-cyan { color: #66ffff; }
|
|
|
|
|
.ansi-bright-white { color: #ffffff; }
|
2025-04-28 12:25:20 +08:00
|
|
|
|
|
2025-05-29 09:26:44 +08:00
|
|
|
|
.ansi-bg-black { background-color: #000000; }
|
|
|
|
|
.ansi-bg-red { background-color: #ff0000; }
|
|
|
|
|
.ansi-bg-green { background-color: #00ff00; }
|
|
|
|
|
.ansi-bg-yellow { background-color: #ffff00; }
|
|
|
|
|
.ansi-bg-blue { background-color: #0000ff; }
|
|
|
|
|
.ansi-bg-magenta { background-color: #ff00ff; }
|
|
|
|
|
.ansi-bg-cyan { background-color: #00ffff; }
|
|
|
|
|
.ansi-bg-white { background-color: #ffffff; }
|
2025-04-28 12:25:20 +08:00
|
|
|
|
|
2025-05-29 09:26:44 +08:00
|
|
|
|
.ansi-bold { font-weight: bold; }
|
|
|
|
|
.ansi-italic { font-style: italic; }
|
|
|
|
|
.ansi-underline { text-decoration: underline; }
|
|
|
|
|
|
|
|
|
|
button {
|
|
|
|
|
padding: 6px 12px;
|
|
|
|
|
background-color: #1976d2;
|
|
|
|
|
color: white;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
min-width: 80px;
|
|
|
|
|
font-size: 0.9em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
button:hover {
|
|
|
|
|
background-color: #1565c0;
|
|
|
|
|
}
|
2025-04-28 12:25:20 +08:00
|
|
|
|
|
2025-05-29 09:26:44 +08:00
|
|
|
|
button:disabled {
|
|
|
|
|
background-color: #cccccc;
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
}
|
2025-05-29 10:01:36 +08:00
|
|
|
|
|
|
|
|
|
.button-group {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.simulation-button {
|
|
|
|
|
background-color: #4caf50;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.simulation-button:hover {
|
|
|
|
|
background-color: #45a049;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.simulation-button:disabled {
|
|
|
|
|
background-color: #cccccc;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.pause-button {
|
|
|
|
|
background-color: #ff9800;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.pause-button:hover {
|
|
|
|
|
background-color: #f57c00;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.pause-button:disabled {
|
|
|
|
|
background-color: #cccccc;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stop-button {
|
|
|
|
|
background-color: #f44336;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stop-button:hover {
|
|
|
|
|
background-color: #d32f2f;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stop-button:disabled {
|
|
|
|
|
background-color: #cccccc;
|
|
|
|
|
}
|
2025-04-28 12:25:20 +08:00
|
|
|
|
</style>
|
|
|
|
|
<div class="test-container">
|
|
|
|
|
<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">
|
2025-05-29 09:26:44 +08:00
|
|
|
|
<div class="output-header">
|
|
|
|
|
<span>运行输出</span>
|
|
|
|
|
<div id="message"></div>
|
2025-05-29 10:01:36 +08:00
|
|
|
|
<div class="button-group">
|
|
|
|
|
<button id="run-button">运行测试</button>
|
|
|
|
|
<button id="run-simulation-button" class="simulation-button">运行仿真</button>
|
|
|
|
|
<button id="pause-simulation-button" class="pause-button" disabled>暂停仿真</button>
|
|
|
|
|
<button id="stop-simulation-button" class="stop-button" disabled>结束仿真</button>
|
|
|
|
|
</div>
|
2025-05-29 09:26:44 +08:00
|
|
|
|
</div>
|
2025-04-28 12:25:20 +08:00
|
|
|
|
<div id="output-content" class="output-content">等待测试运行...</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
// 添加事件监听器
|
|
|
|
|
const runButton = this.shadowRoot.querySelector('#run-button');
|
|
|
|
|
runButton.addEventListener('click', () => this.runTest());
|
2025-05-29 10:01:36 +08:00
|
|
|
|
|
|
|
|
|
const runSimulationButton = this.shadowRoot.querySelector('#run-simulation-button');
|
|
|
|
|
runSimulationButton.addEventListener('click', () => this.runSimulation());
|
|
|
|
|
|
|
|
|
|
const pauseSimulationButton = this.shadowRoot.querySelector('#pause-simulation-button');
|
|
|
|
|
pauseSimulationButton.addEventListener('click', () => this.pauseSimulation());
|
|
|
|
|
|
|
|
|
|
const stopSimulationButton = this.shadowRoot.querySelector('#stop-simulation-button');
|
|
|
|
|
stopSimulationButton.addEventListener('click', () => this.stopSimulation());
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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, ''');
|
|
|
|
|
}
|
2025-05-29 10:01:36 +08:00
|
|
|
|
|
|
|
|
|
async pauseSimulation() {
|
|
|
|
|
const simulationId = this.currentSimulationId;
|
|
|
|
|
if (!simulationId) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const button = this.shadowRoot.querySelector('#pause-simulation-button');
|
|
|
|
|
|
2025-05-29 14:38:09 +08:00
|
|
|
|
// 检查DDS状态
|
|
|
|
|
const ddsStatusResponse = await fetch('/api/dds-monitor/status');
|
|
|
|
|
if (!ddsStatusResponse.ok) {
|
|
|
|
|
throw new Error('获取DDS状态失败');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ddsStatus = await ddsStatusResponse.json();
|
|
|
|
|
if (!ddsStatus.isInitialized) {
|
|
|
|
|
throw new Error('DDS监控未初始化');
|
|
|
|
|
}
|
2025-06-06 11:02:12 +08:00
|
|
|
|
|
|
|
|
|
// 初始化引擎控制
|
|
|
|
|
const initResponse = await fetch('/api/system-control/initialize', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!initResponse.ok) {
|
|
|
|
|
throw new Error('初始化引擎控制失败');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const initResult = await initResponse.json();
|
|
|
|
|
if (!initResult.success) {
|
|
|
|
|
throw new Error(initResult.message || '初始化引擎控制失败');
|
|
|
|
|
}
|
2025-05-29 14:38:09 +08:00
|
|
|
|
|
|
|
|
|
// 根据当前暂停状态调用不同的接口
|
|
|
|
|
const apiEndpoint = this.isPaused ? '/api/system-control/resume' : '/api/system-control/pause';
|
|
|
|
|
const response = await fetch(apiEndpoint, {
|
2025-05-29 10:01:36 +08:00
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json'
|
2025-05-29 14:38:09 +08:00
|
|
|
|
}
|
2025-05-29 10:01:36 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error('操作失败');
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-29 14:38:09 +08:00
|
|
|
|
const result = await response.json();
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
throw new Error(result.message || '操作失败');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新暂停状态
|
|
|
|
|
this.isPaused = !this.isPaused;
|
2025-05-29 10:01:36 +08:00
|
|
|
|
// 更新按钮状态
|
2025-05-29 14:38:09 +08:00
|
|
|
|
button.textContent = this.isPaused ? '继续仿真' : '暂停仿真';
|
|
|
|
|
this.showMessage(this.isPaused ? '仿真已暂停' : '仿真已继续');
|
2025-05-29 10:01:36 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('暂停/继续仿真失败:', error);
|
|
|
|
|
this.showError('操作失败: ' + error.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async stopSimulation() {
|
|
|
|
|
const simulationId = this.currentSimulationId;
|
|
|
|
|
if (!simulationId) return;
|
|
|
|
|
|
|
|
|
|
try {
|
2025-05-29 14:38:09 +08:00
|
|
|
|
// 检查DDS状态
|
|
|
|
|
const ddsStatusResponse = await fetch('/api/dds-monitor/status');
|
|
|
|
|
if (!ddsStatusResponse.ok) {
|
|
|
|
|
throw new Error('获取DDS状态失败');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ddsStatus = await ddsStatusResponse.json();
|
|
|
|
|
if (!ddsStatus.isInitialized) {
|
|
|
|
|
throw new Error('DDS监控未初始化');
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 11:02:12 +08:00
|
|
|
|
// 初始化引擎控制
|
|
|
|
|
const initResponse = await fetch('/api/system-control/initialize', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!initResponse.ok) {
|
|
|
|
|
throw new Error('初始化引擎控制失败');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const initResult = await initResponse.json();
|
|
|
|
|
if (!initResult.success) {
|
|
|
|
|
throw new Error(initResult.message || '初始化引擎控制失败');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 使用引擎控制接口停止仿真
|
2025-05-29 14:38:09 +08:00
|
|
|
|
const response = await fetch('/api/system-control/stop', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error('停止引擎失败');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
throw new Error(result.message || '停止引擎失败');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 等待5秒
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
|
|
|
|
|
|
|
|
// 调用老接口确保完全停止
|
|
|
|
|
const fallbackResponse = await fetch('/api/stop-simulation', {
|
2025-05-29 10:01:36 +08:00
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
id: simulationId
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
|
2025-05-29 14:38:09 +08:00
|
|
|
|
if (!fallbackResponse.ok) {
|
|
|
|
|
console.warn('调用老接口停止仿真失败,但引擎已停止');
|
2025-05-29 10:01:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 关闭SSE连接
|
|
|
|
|
if (this.eventSource) {
|
|
|
|
|
this.eventSource.close();
|
|
|
|
|
this.eventSource = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 重置按钮状态
|
|
|
|
|
this.resetSimulationButtons();
|
|
|
|
|
this.currentSimulationId = null;
|
|
|
|
|
this.showSuccess('仿真已停止');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('停止仿真失败:', error);
|
|
|
|
|
this.showError('停止仿真失败: ' + error.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resetSimulationButtons() {
|
|
|
|
|
const runButton = this.shadowRoot.querySelector('#run-simulation-button');
|
|
|
|
|
const pauseButton = this.shadowRoot.querySelector('#pause-simulation-button');
|
|
|
|
|
const stopButton = this.shadowRoot.querySelector('#stop-simulation-button');
|
|
|
|
|
|
|
|
|
|
runButton.disabled = false;
|
|
|
|
|
runButton.textContent = '运行仿真';
|
|
|
|
|
pauseButton.disabled = true;
|
|
|
|
|
stopButton.disabled = true;
|
|
|
|
|
pauseButton.textContent = '暂停仿真';
|
2025-05-29 14:38:09 +08:00
|
|
|
|
this.isPaused = false;
|
2025-05-29 10:01:36 +08:00
|
|
|
|
}
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-29 10:01:36 +08:00
|
|
|
|
customElements.define('run-sim', RunSim);
|