2025-04-28 12:25:20 +08:00
|
|
|
|
class SimulationMonitor extends HTMLElement {
|
|
|
|
|
constructor() {
|
|
|
|
|
super();
|
|
|
|
|
this.attachShadow({ mode: 'open' });
|
2025-05-14 14:30:38 +08:00
|
|
|
|
this.monitorStatus = {
|
|
|
|
|
isMonitoring: false,
|
|
|
|
|
lastError: null
|
|
|
|
|
};
|
|
|
|
|
this.systemInfo = null;
|
|
|
|
|
this.threadInfo = null;
|
|
|
|
|
this.statusCheckInterval = null;
|
|
|
|
|
this.chart = null;
|
|
|
|
|
this.chartInitialized = false;
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
connectedCallback() {
|
|
|
|
|
this.render();
|
2025-05-14 14:30:38 +08:00
|
|
|
|
// 等待组件完全加载
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.initializeComponent();
|
|
|
|
|
}, 100);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
initializeComponent() {
|
|
|
|
|
if (this.chartInitialized) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
this.initChart();
|
|
|
|
|
this.startStatusCheck();
|
|
|
|
|
this.chartInitialized = true;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('初始化组件失败:', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
disconnectedCallback() {
|
|
|
|
|
this.stopStatusCheck();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
startStatusCheck() {
|
|
|
|
|
this.checkMonitorStatus();
|
|
|
|
|
this.statusCheckInterval = setInterval(() => {
|
|
|
|
|
this.checkMonitorStatus();
|
|
|
|
|
}, 1000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stopStatusCheck() {
|
|
|
|
|
if (this.statusCheckInterval) {
|
|
|
|
|
clearInterval(this.statusCheckInterval);
|
|
|
|
|
this.statusCheckInterval = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async checkMonitorStatus() {
|
|
|
|
|
try {
|
|
|
|
|
// 获取监控状态
|
|
|
|
|
const statusResponse = await fetch('/api/system-monitor/status');
|
|
|
|
|
const statusData = await statusResponse.json();
|
|
|
|
|
this.monitorStatus = statusData;
|
|
|
|
|
|
|
|
|
|
if (this.monitorStatus.isMonitoring) {
|
|
|
|
|
// 获取系统信息
|
|
|
|
|
const systemResponse = await fetch('/api/system-monitor/system-info');
|
|
|
|
|
const systemData = await systemResponse.json();
|
|
|
|
|
this.systemInfo = systemData.data;
|
|
|
|
|
|
|
|
|
|
// 获取线程信息
|
|
|
|
|
const threadResponse = await fetch('/api/system-monitor/thread-info');
|
|
|
|
|
const threadData = await threadResponse.json();
|
|
|
|
|
this.threadInfo = threadData.data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.updateUI();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取监控状态失败:', error);
|
|
|
|
|
this.monitorStatus.lastError = error.message;
|
|
|
|
|
this.updateUI();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-14 16:42:09 +08:00
|
|
|
|
getStatusDisplay(status) {
|
|
|
|
|
switch (status) {
|
|
|
|
|
case 0:
|
|
|
|
|
return { text: '未运行', color: '#999999' };
|
|
|
|
|
case 1:
|
|
|
|
|
return { text: '运行中', color: '#4CAF50' };
|
|
|
|
|
case 2:
|
|
|
|
|
return { text: '暂停中', color: '#FF9800' };
|
|
|
|
|
case 3:
|
|
|
|
|
return { text: '错误', color: '#f44336' };
|
|
|
|
|
default:
|
|
|
|
|
return { text: '未知状态', color: '#f44336' };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getCoreStatusDisplay(status) {
|
|
|
|
|
switch (status) {
|
|
|
|
|
case 0:
|
|
|
|
|
return { text: '未加载', color: '#999999' };
|
|
|
|
|
case 1:
|
|
|
|
|
return { text: '初始化完成', color: '#FFC107' };
|
|
|
|
|
case 2:
|
|
|
|
|
return { text: '正常', color: '#4CAF50' };
|
|
|
|
|
case 3:
|
|
|
|
|
return { text: '异常', color: '#f44336' };
|
|
|
|
|
default:
|
|
|
|
|
return { text: '未知状态', color: '#f44336' };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getThreadStatusDisplay(status) {
|
|
|
|
|
switch (status) {
|
|
|
|
|
case 0:
|
|
|
|
|
return { text: '未运行', color: '#999999' };
|
|
|
|
|
case 1:
|
|
|
|
|
return { text: '运行中', color: '#4CAF50' };
|
|
|
|
|
case 2:
|
|
|
|
|
return { text: '暂停中', color: '#FF9800' };
|
|
|
|
|
case 3:
|
|
|
|
|
return { text: '错误', color: '#f44336' };
|
|
|
|
|
default:
|
|
|
|
|
return { text: '未知状态', color: '#f44336' };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-14 14:30:38 +08:00
|
|
|
|
updateUI() {
|
|
|
|
|
const input = this.shadowRoot.querySelector('.domain-input');
|
|
|
|
|
const startButton = this.shadowRoot.querySelector('.start-button');
|
|
|
|
|
const stopButton = this.shadowRoot.querySelector('.stop-button');
|
|
|
|
|
const statusDisplay = this.shadowRoot.querySelector('.status-display');
|
|
|
|
|
const engineInfo = this.shadowRoot.querySelector('#engine-info');
|
|
|
|
|
const coreStatus = this.shadowRoot.querySelector('#core-status');
|
|
|
|
|
const threadTableBody = this.shadowRoot.querySelector('#thread-table-body');
|
|
|
|
|
|
|
|
|
|
if (this.monitorStatus.isMonitoring) {
|
|
|
|
|
input.disabled = true;
|
|
|
|
|
startButton.disabled = true;
|
|
|
|
|
stopButton.disabled = false;
|
|
|
|
|
} else {
|
|
|
|
|
input.disabled = false;
|
|
|
|
|
startButton.disabled = false;
|
|
|
|
|
stopButton.disabled = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新状态显示
|
|
|
|
|
statusDisplay.textContent = `监控状态: ${this.monitorStatus.isMonitoring ? '运行中' : '已停止'}`;
|
|
|
|
|
if (this.monitorStatus.lastError) {
|
|
|
|
|
statusDisplay.textContent += ` (错误: ${this.monitorStatus.lastError})`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新引擎信息
|
|
|
|
|
const engineInfoFields = [
|
|
|
|
|
{ label: '引擎名称', key: 'name' },
|
|
|
|
|
{ label: '引擎ID', key: 'id' },
|
|
|
|
|
{ label: '引擎状态', key: 'status' },
|
|
|
|
|
{ label: '引擎亲和性', key: 'affinity' },
|
|
|
|
|
{ label: '线程数', key: 'threadCount' }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
engineInfo.innerHTML = `
|
|
|
|
|
<div class="status-grid">
|
2025-05-14 16:42:09 +08:00
|
|
|
|
${engineInfoFields.map(field => {
|
|
|
|
|
let value = '未知';
|
|
|
|
|
let color = '#666';
|
|
|
|
|
|
|
|
|
|
if (this.monitorStatus.isMonitoring && this.systemInfo?.engineInfo) {
|
|
|
|
|
if (field.key === 'status') {
|
|
|
|
|
const status = this.systemInfo.engineInfo[field.key];
|
|
|
|
|
const statusInfo = this.getStatusDisplay(status);
|
|
|
|
|
value = statusInfo.text;
|
|
|
|
|
color = statusInfo.color;
|
|
|
|
|
} else {
|
|
|
|
|
value = this.systemInfo.engineInfo[field.key] || '未知';
|
|
|
|
|
}
|
|
|
|
|
} else if (field.key === 'status') {
|
|
|
|
|
const statusInfo = this.getStatusDisplay(0);
|
|
|
|
|
value = statusInfo.text;
|
|
|
|
|
color = statusInfo.color;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
<div class="status-item">
|
|
|
|
|
<div class="status-label">${field.label}</div>
|
|
|
|
|
<div class="status-value" style="color: ${color}">${value}</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}).join('')}
|
2025-05-14 14:30:38 +08:00
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
// 更新核心状态
|
|
|
|
|
const coreStatusFields = [
|
2025-05-14 16:42:09 +08:00
|
|
|
|
{ label: '主框架状态', key: 'fw' },
|
|
|
|
|
{ label: '时间管理器状态', key: 'tm' },
|
|
|
|
|
{ label: '事件管理器状态', key: 'em' },
|
|
|
|
|
{ label: '环境管理器状态', key: 'sd' },
|
|
|
|
|
{ label: '线程管理器状态', key: 'thm' },
|
|
|
|
|
{ label: '模型管理器状态', key: 'mm' },
|
|
|
|
|
{ label: '服务管理器状态', key: 'sm' },
|
|
|
|
|
{ label: 'DDS管理器状态', key: 'dm' }
|
2025-05-14 14:30:38 +08:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// 确保coreStatus元素存在
|
|
|
|
|
if (!coreStatus) {
|
|
|
|
|
console.error('找不到核心状态元素');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 无论是否有数据,都显示状态项
|
|
|
|
|
coreStatus.innerHTML = `
|
|
|
|
|
<div class="status-grid">
|
2025-05-14 16:42:09 +08:00
|
|
|
|
${coreStatusFields.map(field => {
|
|
|
|
|
const status = this.monitorStatus.isMonitoring && this.systemInfo?.coreStatus ?
|
|
|
|
|
this.systemInfo.coreStatus[field.key] : 0;
|
|
|
|
|
const statusInfo = this.getCoreStatusDisplay(status);
|
|
|
|
|
return `
|
|
|
|
|
<div class="status-item">
|
|
|
|
|
<div class="status-label">${field.label}</div>
|
|
|
|
|
<div class="status-value" style="color: ${statusInfo.color}">${statusInfo.text}</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}).join('')}
|
2025-05-14 14:30:38 +08:00
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
// 更新线程表格
|
|
|
|
|
if (this.monitorStatus.isMonitoring && this.threadInfo && Array.isArray(this.threadInfo)) {
|
2025-05-14 16:42:09 +08:00
|
|
|
|
threadTableBody.innerHTML = this.threadInfo.map(thread => {
|
|
|
|
|
const statusInfo = this.getThreadStatusDisplay(thread.status);
|
|
|
|
|
return `
|
|
|
|
|
<tr>
|
|
|
|
|
<td>${thread.name || '未知'}</td>
|
|
|
|
|
<td>${thread.id || '未知'}</td>
|
|
|
|
|
<td style="color: ${statusInfo.color}">${statusInfo.text}</td>
|
|
|
|
|
<td>${thread.priority || '未知'}</td>
|
|
|
|
|
<td>${thread.runCount || '0'}</td>
|
|
|
|
|
<td>${(thread.setFrequency || 0).toFixed(2)}</td>
|
|
|
|
|
<td>${(thread.avgFrequency || 0).toFixed(2)}</td>
|
|
|
|
|
<td>${(thread.maxFrequency || 0).toFixed(2)}</td>
|
|
|
|
|
<td>${(thread.minFrequency || 0).toFixed(2)}</td>
|
|
|
|
|
<td>${(thread.setPeriod || 0).toFixed(2)}</td>
|
|
|
|
|
<td>${(thread.avgPeriod || 0).toFixed(2)}</td>
|
|
|
|
|
<td>${(thread.maxPeriod || 0).toFixed(2)}</td>
|
|
|
|
|
<td>${(thread.minPeriod || 0).toFixed(2)}</td>
|
|
|
|
|
</tr>
|
|
|
|
|
`;
|
|
|
|
|
}).join('');
|
2025-05-14 14:30:38 +08:00
|
|
|
|
|
|
|
|
|
// 更新图表数据
|
|
|
|
|
this.updateChartData();
|
|
|
|
|
} else {
|
2025-05-14 16:42:09 +08:00
|
|
|
|
threadTableBody.innerHTML = '<tr><td colspan="13" style="text-align: center;">暂无线程信息</td></tr>';
|
2025-05-14 14:30:38 +08:00
|
|
|
|
// 清空图表数据
|
|
|
|
|
if (this.chart) {
|
|
|
|
|
this.chart.data.labels = [];
|
|
|
|
|
this.chart.data.datasets = [];
|
|
|
|
|
this.chart.update();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async startMonitoring() {
|
|
|
|
|
const domainId = this.shadowRoot.querySelector('.domain-input').value.trim();
|
|
|
|
|
|
|
|
|
|
// 验证域ID是否为有效的数字字符串
|
|
|
|
|
if (!/^\d+$/.test(domainId)) {
|
|
|
|
|
console.error('域ID必须是有效的数字');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 首先检查DDS监控状态
|
|
|
|
|
const ddsStatusResponse = await fetch('/api/dds-monitor/status');
|
|
|
|
|
const ddsStatusData = await ddsStatusResponse.json();
|
|
|
|
|
|
|
|
|
|
// 如果DDS监控未初始化,先初始化DDS监控
|
|
|
|
|
if (!ddsStatusData.isInitialized) {
|
|
|
|
|
const ddsInitResponse = await fetch('/api/dds-monitor/initialize', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({ domainId })
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!ddsInitResponse.ok) {
|
|
|
|
|
const errorData = await ddsInitResponse.json();
|
|
|
|
|
console.error('DDS监控初始化失败:', errorData.error);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 启动系统监控
|
|
|
|
|
const response = await fetch('/api/system-monitor/start', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({ domainId })
|
|
|
|
|
});
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
this.monitorStatus = data.status;
|
|
|
|
|
this.updateUI();
|
|
|
|
|
} else {
|
|
|
|
|
console.error('启动监控失败:', data.error);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('启动监控失败:', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async stopMonitoring() {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch('/api/system-monitor/stop', {
|
|
|
|
|
method: 'POST'
|
|
|
|
|
});
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
this.monitorStatus = data.status;
|
|
|
|
|
this.updateUI();
|
|
|
|
|
} else {
|
|
|
|
|
console.error('停止监控失败:', data.error);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('停止监控失败:', error);
|
|
|
|
|
}
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
render() {
|
|
|
|
|
this.shadowRoot.innerHTML = `
|
|
|
|
|
<style>
|
|
|
|
|
:host {
|
|
|
|
|
display: block;
|
|
|
|
|
height: 100%;
|
|
|
|
|
overflow: auto;
|
|
|
|
|
padding: 16px;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.monitor-container {
|
2025-05-14 14:30:38 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
height: 100%;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.toolbar-section {
|
2025-04-28 12:25:20 +08:00
|
|
|
|
background-color: white;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
|
|
|
padding: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-14 14:30:38 +08:00
|
|
|
|
.content-container {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 300px 1fr;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
flex: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.left-panel {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.right-panel {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.panel-section {
|
|
|
|
|
background-color: white;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
|
|
|
padding: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.toolbar {
|
2025-04-28 12:25:20 +08:00
|
|
|
|
display: flex;
|
2025-05-14 14:30:38 +08:00
|
|
|
|
gap: 12px;
|
|
|
|
|
align-items: center;
|
2025-04-28 12:25:20 +08:00
|
|
|
|
justify-content: space-between;
|
2025-05-14 14:30:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.toolbar-left {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 12px;
|
2025-04-28 12:25:20 +08:00
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-14 14:30:38 +08:00
|
|
|
|
.input-group {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.input-label {
|
|
|
|
|
font-size: 14px;
|
2025-04-28 12:25:20 +08:00
|
|
|
|
color: #333;
|
2025-05-14 14:30:38 +08:00
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.domain-input {
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
border: 1px solid #ddd;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
width: 200px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.domain-input:disabled {
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.control-button {
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: background-color 0.2s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.control-button:disabled {
|
|
|
|
|
opacity: 0.6;
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.start-button {
|
|
|
|
|
background-color: #4CAF50;
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.start-button:hover:not(:disabled) {
|
|
|
|
|
background-color: #45a049;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stop-button {
|
|
|
|
|
background-color: #f44336;
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stop-button:hover:not(:disabled) {
|
|
|
|
|
background-color: #da190b;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-display {
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
color: #666;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.info-grid {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.info-item {
|
|
|
|
|
background-color: #f8f9fa;
|
|
|
|
|
padding: 12px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
border: 1px solid #e9ecef;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.info-item .label {
|
|
|
|
|
color: #666;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
display: block;
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.info-item .value {
|
|
|
|
|
color: #333;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
h3 {
|
|
|
|
|
margin: 0 0 12px 0;
|
|
|
|
|
color: #333;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.thread-table {
|
|
|
|
|
width: 100%;
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.thread-table th,
|
|
|
|
|
.thread-table td {
|
|
|
|
|
padding: 8px;
|
|
|
|
|
text-align: left;
|
|
|
|
|
border-bottom: 1px solid #e9ecef;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.thread-table th {
|
|
|
|
|
background-color: #f8f9fa;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: #333;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.thread-table tr:hover {
|
|
|
|
|
background-color: #f8f9fa;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chart-container {
|
|
|
|
|
position: relative;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 300px;
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#thread-chart {
|
|
|
|
|
width: 100% !important;
|
|
|
|
|
height: 100% !important;
|
|
|
|
|
display: block;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chart-controls {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.switch {
|
|
|
|
|
position: relative;
|
|
|
|
|
display: inline-block;
|
|
|
|
|
width: 60px;
|
|
|
|
|
height: 34px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.switch input {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
width: 0;
|
|
|
|
|
height: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.slider {
|
|
|
|
|
position: absolute;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
background-color: #ccc;
|
|
|
|
|
transition: .4s;
|
|
|
|
|
border-radius: 34px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.slider:before {
|
|
|
|
|
position: absolute;
|
|
|
|
|
content: "";
|
|
|
|
|
height: 26px;
|
|
|
|
|
width: 26px;
|
|
|
|
|
left: 4px;
|
|
|
|
|
bottom: 4px;
|
|
|
|
|
background-color: white;
|
|
|
|
|
transition: .4s;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
input:checked + .slider {
|
|
|
|
|
background-color: #2196F3;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
input:checked + .slider:before {
|
|
|
|
|
transform: translateX(26px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-grid {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 8px;
|
|
|
|
|
background-color: #f8f9fa;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
border: 1px solid #e9ecef;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-label {
|
|
|
|
|
flex: 1;
|
|
|
|
|
color: #666;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-value {
|
|
|
|
|
flex: 1;
|
|
|
|
|
color: #333;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
text-align: right;
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
<div class="monitor-container">
|
2025-05-14 14:30:38 +08:00
|
|
|
|
<div class="toolbar-section">
|
|
|
|
|
<div class="toolbar">
|
|
|
|
|
<div class="toolbar-left">
|
|
|
|
|
<div class="input-group">
|
|
|
|
|
<label class="input-label">DDS通信域ID:</label>
|
|
|
|
|
<input type="text" class="domain-input" value="10">
|
|
|
|
|
</div>
|
|
|
|
|
<button class="control-button start-button" onclick="this.getRootNode().host.startMonitoring()">开始监控</button>
|
|
|
|
|
<button class="control-button stop-button" onclick="this.getRootNode().host.stopMonitoring()">停止监控</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="status-display">监控状态: 未启动</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="content-container">
|
|
|
|
|
<div class="left-panel">
|
|
|
|
|
<div class="panel-section">
|
|
|
|
|
<h3>引擎信息</h3>
|
|
|
|
|
<div class="info-grid" id="engine-info"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="panel-section">
|
|
|
|
|
<h3>核心状态</h3>
|
|
|
|
|
<div class="info-grid" id="core-status"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="right-panel">
|
|
|
|
|
<div class="panel-section">
|
|
|
|
|
<h3>线程信息</h3>
|
|
|
|
|
<table class="thread-table">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>线程名称</th>
|
|
|
|
|
<th>线程ID</th>
|
|
|
|
|
<th>状态</th>
|
|
|
|
|
<th>优先级</th>
|
|
|
|
|
<th>运行次数</th>
|
2025-05-14 16:42:09 +08:00
|
|
|
|
<th>设定频率(Hz)</th>
|
|
|
|
|
<th>平均频率(Hz)</th>
|
|
|
|
|
<th>最大频率(Hz)</th>
|
|
|
|
|
<th>最小频率(Hz)</th>
|
|
|
|
|
<th>设定周期(ms)</th>
|
|
|
|
|
<th>平均周期(ms)</th>
|
|
|
|
|
<th>最大周期(ms)</th>
|
|
|
|
|
<th>最小周期(ms)</th>
|
2025-05-14 14:30:38 +08:00
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody id="thread-table-body"></tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="panel-section">
|
|
|
|
|
<h3>线程监控</h3>
|
|
|
|
|
<div class="chart-container">
|
|
|
|
|
<canvas id="thread-chart"></canvas>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="chart-controls">
|
|
|
|
|
<span>显示类型:</span>
|
|
|
|
|
<label class="switch">
|
|
|
|
|
<input type="checkbox" id="display-type-switch">
|
|
|
|
|
<span class="slider"></span>
|
|
|
|
|
</label>
|
|
|
|
|
<span id="display-type-label">频率</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-04-28 12:25:20 +08:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
2025-05-14 14:30:38 +08:00
|
|
|
|
|
|
|
|
|
initChart() {
|
|
|
|
|
const chartElement = this.shadowRoot.querySelector('#thread-chart');
|
|
|
|
|
if (!chartElement) {
|
|
|
|
|
console.error('找不到图表元素');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 确保 Chart.js 已加载
|
|
|
|
|
if (typeof Chart === 'undefined') {
|
|
|
|
|
console.error('Chart.js 未加载');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 创建图表实例
|
|
|
|
|
const ctx = chartElement.getContext('2d');
|
|
|
|
|
if (!ctx) {
|
|
|
|
|
console.error('无法获取 canvas 上下文');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 销毁已存在的图表实例
|
|
|
|
|
if (this.chart) {
|
|
|
|
|
this.chart.destroy();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 创建新的图表实例
|
|
|
|
|
this.chart = new Chart(ctx, {
|
|
|
|
|
type: 'line',
|
|
|
|
|
data: {
|
|
|
|
|
labels: [],
|
|
|
|
|
datasets: []
|
|
|
|
|
},
|
|
|
|
|
options: {
|
|
|
|
|
responsive: true,
|
|
|
|
|
maintainAspectRatio: false,
|
|
|
|
|
scales: {
|
|
|
|
|
y: {
|
|
|
|
|
beginAtZero: true
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
animation: false,
|
|
|
|
|
plugins: {
|
|
|
|
|
legend: {
|
|
|
|
|
display: true
|
|
|
|
|
},
|
|
|
|
|
tooltip: {
|
|
|
|
|
enabled: false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 监听显示类型切换
|
|
|
|
|
const switchInput = this.shadowRoot.querySelector('#display-type-switch');
|
|
|
|
|
const displayTypeLabel = this.shadowRoot.querySelector('#display-type-label');
|
|
|
|
|
|
|
|
|
|
if (switchInput && displayTypeLabel) {
|
|
|
|
|
switchInput.addEventListener('change', (e) => {
|
|
|
|
|
const isFrequency = e.target.checked;
|
|
|
|
|
displayTypeLabel.textContent = isFrequency ? '频率' : '周期';
|
|
|
|
|
this.updateChartDisplayType(isFrequency);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('初始化图表失败:', error);
|
|
|
|
|
this.chart = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateChartData() {
|
|
|
|
|
if (!this.chart || !this.threadInfo || !Array.isArray(this.threadInfo)) return;
|
|
|
|
|
|
|
|
|
|
const isFrequency = this.shadowRoot.querySelector('#display-type-switch').checked;
|
|
|
|
|
const currentTime = new Date().toLocaleTimeString();
|
|
|
|
|
|
|
|
|
|
// 确保图表数据集存在
|
|
|
|
|
if (!this.chart.data.datasets || this.chart.data.datasets.length === 0) {
|
|
|
|
|
this.updateChartDisplayType(isFrequency);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新数据
|
|
|
|
|
const newData = {
|
|
|
|
|
labels: [...this.chart.data.labels, currentTime].slice(-30),
|
|
|
|
|
datasets: this.threadInfo.map((thread, index) => {
|
|
|
|
|
const dataset = this.chart.data.datasets[index] || {
|
|
|
|
|
label: thread.name,
|
|
|
|
|
data: [],
|
|
|
|
|
borderColor: this.getRandomColor(),
|
|
|
|
|
fill: false
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const value = isFrequency ? (thread.currentFrequency || 0) : (thread.currentPeriod || 0);
|
|
|
|
|
dataset.data = [...dataset.data, value].slice(-30);
|
|
|
|
|
|
|
|
|
|
return dataset;
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 使用新数据更新图表
|
|
|
|
|
this.chart.data = newData;
|
|
|
|
|
this.chart.update('none'); // 使用 'none' 模式更新,避免触发动画和事件
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateChartDisplayType(isFrequency) {
|
|
|
|
|
if (!this.threadInfo) return;
|
|
|
|
|
|
|
|
|
|
const datasets = this.threadInfo.map(thread => ({
|
|
|
|
|
label: thread.name,
|
|
|
|
|
data: [],
|
|
|
|
|
borderColor: this.getRandomColor(),
|
|
|
|
|
fill: false
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
this.chart.data.datasets = datasets;
|
|
|
|
|
this.chart.update('none'); // 使用 'none' 模式更新
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getRandomColor() {
|
|
|
|
|
const letters = '0123456789ABCDEF';
|
|
|
|
|
let color = '#';
|
|
|
|
|
for (let i = 0; i < 6; i++) {
|
|
|
|
|
color += letters[Math.floor(Math.random() * 16)];
|
|
|
|
|
}
|
|
|
|
|
return color;
|
|
|
|
|
}
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
customElements.define('simulation-monitor', SimulationMonitor);
|