1050 lines
38 KiB
JavaScript
Raw Normal View History

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;
this.currentSimulationId = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 3;
2025-04-28 12:25:20 +08:00
}
connectedCallback() {
this.render();
2025-05-29 09:26:44 +08:00
this.loadModelGroups();
this.loadServices();
// 检查并连接到已有的仿真
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
// 调用后端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
// 根据测试结果更新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}`;
} finally {
2025-04-28 12:25:20 +08:00
// 重置UI
const runButton = this.shadowRoot.querySelector('#run-button');
runButton.disabled = false;
runButton.textContent = '运行测试';
2025-04-28 12:25:20 +08:00
}
}
2025-05-29 09:26:44 +08:00
// 开始轮询日志文件
startLogFilePolling(logFile) {
// 清除之前的轮询
if (this.logFilePollingInterval) {
clearInterval(this.logFilePollingInterval);
this.logFilePollingInterval = null;
}
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
// 连接到SSE事件源获取实时输出
connectToEventSource(simulationId) {
// 关闭之前的连接
if (this.eventSource) {
this.eventSource.close();
}
// 创建新的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';
// 准备启动参数
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}`;
}
}
// 添加新方法:检查并连接到已有的仿真
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获取输出
this.connectToEventSource(this.currentSimulationId);
// 更新状态为已连接
this.showSuccess('已连接到运行中的仿真');
// 重置重连尝试次数
this.reconnectAttempts = 0;
} else {
// 如果没有运行中的仿真重置UI
this.resetSimulationButtons();
}
} catch (error) {
console.error('检查XNEngine进程失败:', error);
this.showError('检查仿真状态失败: ' + error.message);
this.resetSimulationButtons();
}
}
// 添加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
}
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;
}
.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>
<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());
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
async pauseSimulation() {
const simulationId = this.currentSimulationId;
if (!simulationId) return;
try {
const button = this.shadowRoot.querySelector('#pause-simulation-button');
const isPaused = button.textContent === '继续仿真';
// 调用后端API暂停/继续仿真
const response = await fetch('/api/pause-simulation', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: simulationId,
pause: !isPaused
})
});
if (!response.ok) {
throw new Error('操作失败');
}
// 更新按钮状态
button.textContent = isPaused ? '暂停仿真' : '继续仿真';
this.showMessage(isPaused ? '仿真已继续' : '仿真已暂停');
} catch (error) {
console.error('暂停/继续仿真失败:', error);
this.showError('操作失败: ' + error.message);
}
}
async stopSimulation() {
const simulationId = this.currentSimulationId;
if (!simulationId) return;
try {
// 调用后端API停止仿真
const response = await fetch('/api/stop-simulation', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: simulationId
})
});
if (!response.ok) {
throw new Error('停止仿真失败');
}
// 关闭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-04-28 12:25:20 +08:00
}
customElements.define('run-sim', RunSim);