808 lines
28 KiB
JavaScript
808 lines
28 KiB
JavaScript
class RunTest extends HTMLElement {
|
||
constructor() {
|
||
super();
|
||
this.attachShadow({ mode: 'open' });
|
||
this.scenarioFiles = [];
|
||
this.currentScenario = null;
|
||
this.modelGroups = [];
|
||
this.services = [];
|
||
this.currentSimulationId = null;
|
||
this.eventSource = null;
|
||
}
|
||
|
||
connectedCallback() {
|
||
this.fetchScenarioFiles();
|
||
this.render();
|
||
}
|
||
|
||
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);
|
||
});
|
||
}
|
||
|
||
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');
|
||
if (stopButton) {
|
||
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, '-test'];
|
||
|
||
// 调用后端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;
|
||
|
||
// 设置SSE连接获取实时输出
|
||
this.connectToEventSource(simulationId);
|
||
|
||
this.showSuccess(`测试已启动`);
|
||
|
||
} catch (error) {
|
||
console.error('执行测试失败:', error);
|
||
this.showError('执行测试失败: ' + error.message);
|
||
|
||
// 显示错误详情
|
||
const outputContent = this.shadowRoot.querySelector('#output-content');
|
||
outputContent.innerHTML += `\n\n执行错误: ${error.message}`;
|
||
|
||
// 重置UI
|
||
this.resetUIAfterCompletion();
|
||
}
|
||
}
|
||
|
||
// 连接到SSE事件源获取实时输出
|
||
connectToEventSource(simulationId) {
|
||
// 关闭之前的连接
|
||
this.closeEventSource();
|
||
|
||
// 创建新的SSE连接
|
||
const url = `/api/simulation-output/${simulationId}`;
|
||
this.eventSource = new EventSource(url);
|
||
|
||
// 标准输出和错误输出
|
||
this.eventSource.addEventListener('output', (event) => {
|
||
try {
|
||
const data = JSON.parse(event.data);
|
||
const outputContent = this.shadowRoot.querySelector('#output-content');
|
||
|
||
// 添加新输出并应用ANSI颜色
|
||
if (data.data) {
|
||
const html = this.convertAnsiToHtml(data.data);
|
||
outputContent.innerHTML += html;
|
||
|
||
// 自动滚动到底部
|
||
outputContent.scrollTop = outputContent.scrollHeight;
|
||
}
|
||
} catch (error) {
|
||
console.error('处理输出事件失败:', error);
|
||
}
|
||
});
|
||
|
||
// 仿真状态
|
||
this.eventSource.addEventListener('status', (event) => {
|
||
try {
|
||
const data = JSON.parse(event.data);
|
||
|
||
if (data.running === false) {
|
||
// 仿真已经不存在或已结束
|
||
this.showMessage(data.message || '测试不存在或已结束');
|
||
|
||
// 重置UI
|
||
this.resetUIAfterCompletion();
|
||
}
|
||
} catch (error) {
|
||
console.error('处理状态事件失败:', error);
|
||
}
|
||
});
|
||
|
||
// 仿真完成
|
||
this.eventSource.addEventListener('completed', (event) => {
|
||
try {
|
||
const data = JSON.parse(event.data);
|
||
|
||
if (data.success) {
|
||
this.showSuccess('测试执行成功');
|
||
} else {
|
||
this.showError(`测试执行失败: ${data.message}`);
|
||
}
|
||
|
||
// 重置UI
|
||
this.resetUIAfterCompletion();
|
||
} catch (error) {
|
||
console.error('处理完成事件失败:', error);
|
||
}
|
||
});
|
||
|
||
// 仿真错误
|
||
this.eventSource.addEventListener('error', (event) => {
|
||
try {
|
||
const data = JSON.parse(event.data);
|
||
this.showError(`测试错误: ${data.message}`);
|
||
|
||
// 重置UI
|
||
this.resetUIAfterCompletion();
|
||
} catch (error) {
|
||
console.error('处理错误事件失败:', error);
|
||
}
|
||
});
|
||
|
||
// 仿真终止
|
||
this.eventSource.addEventListener('terminated', (event) => {
|
||
try {
|
||
const data = JSON.parse(event.data);
|
||
this.showMessage(`测试已终止: ${data.message}`);
|
||
|
||
// 在输出框中添加终止信息
|
||
const outputContent = this.shadowRoot.querySelector('#output-content');
|
||
outputContent.innerHTML += `\n\n测试已终止:${data.message}`;
|
||
|
||
// 重置UI
|
||
this.resetUIAfterCompletion();
|
||
} catch (error) {
|
||
console.error('处理终止事件失败:', error);
|
||
}
|
||
});
|
||
|
||
// 仿真超时
|
||
this.eventSource.addEventListener('timeout', (event) => {
|
||
try {
|
||
const data = JSON.parse(event.data);
|
||
this.showError(`测试超时: ${data.message}`);
|
||
|
||
// 在输出框中添加超时信息
|
||
const outputContent = this.shadowRoot.querySelector('#output-content');
|
||
outputContent.innerHTML += `\n\n测试超时:${data.message}`;
|
||
|
||
// 重置UI
|
||
this.resetUIAfterCompletion();
|
||
} catch (error) {
|
||
console.error('处理超时事件失败:', error);
|
||
}
|
||
});
|
||
|
||
// 连接错误
|
||
this.eventSource.onerror = (error) => {
|
||
console.error('SSE连接错误:', error);
|
||
|
||
// 检查是否已经清理了资源
|
||
if (this.eventSource && this.eventSource.readyState === 2) { // CLOSED
|
||
this.showError('实时输出连接已断开');
|
||
|
||
// 重置UI
|
||
this.resetUIAfterCompletion();
|
||
}
|
||
};
|
||
}
|
||
|
||
// 关闭SSE连接
|
||
closeEventSource() {
|
||
if (this.eventSource) {
|
||
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;
|
||
}
|
||
|
||
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));
|
||
}
|
||
|
||
// 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, ''');
|
||
}
|
||
}
|
||
|
||
customElements.define('run-test', RunTest);
|