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);
|