XNSim/XNSimHtml/components/data-monitor.js

1150 lines
44 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @class DataMonitor
* @extends HTMLElement
* @description 数据监控组件的基础类
*/
class DataMonitor extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.interfaces = []; // 存储接口数据
this.filteredInterfaces = []; // 存储过滤后的接口数据
this.searchText = ''; // 搜索文本
this.searchTimeout = null; // 用于防抖的定时器
this.cursorPosition = 0; // 存储光标位置
this.tableData = []; // 表格数据,存储已添加的接口
this.chart = null; // Chart.js 实例
this.tableHeight = 220; // 表格初始高度(px)
this.isResizing = false; // 是否正在拖动分隔线
this.startY = 0; // 拖动起始Y
this.startTableHeight = 0; // 拖动起始表格高度
this.activeChartIndexes = []; // 当前绘图的行索引
// 列宽数组,初始为像素
this.colWidths = [160, 160, 120, 180, 320]; // px
this.dragColIndex = null;
this.dragStartX = 0;
this.dragStartWidth = 0;
this._colWidthInited = false;
this._resizeEventBinded = false;
this.monitorId = `data_monitor_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; // 添加监控器ID
this.dataUpdateTimer = null; // 数据更新定时器
}
connectedCallback() {
this.loadInterfaces();
// 延迟多次尝试,直到拿到有效宽度
const tryInitColWidth = (tryCount = 0) => {
const section = this.shadowRoot && this.shadowRoot.querySelector('.table-section');
if (section && section.clientWidth > 0) {
const totalWidth = section.clientWidth;
const ratios = [0.17, 0.17, 0.13, 0.19, 0.34];
this.colWidths = ratios.map(r => Math.floor(totalWidth * r));
this._colWidthInited = true;
this.render();
} else if (tryCount < 10) {
setTimeout(() => tryInitColWidth(tryCount + 1), 50);
}
};
tryInitColWidth();
// 只绑定一次全局拖动事件
if (!this._resizeEventBinded) {
window.addEventListener('mousemove', this._onWindowMouseMove = (e) => {
if (this.isResizing) {
const delta = e.clientY - this.startY;
let newHeight = this.startTableHeight + delta;
const minHeight = 60;
const maxHeight = Math.max(120, this.offsetHeight - 180);
if (newHeight < minHeight) newHeight = minHeight;
if (newHeight > maxHeight) newHeight = maxHeight;
this.tableHeight = newHeight;
this.render();
}
});
window.addEventListener('mouseup', this._onWindowMouseUp = () => {
if (this.isResizing) {
this.isResizing = false;
const divider = this.shadowRoot.querySelector('.divider');
if (divider) divider.classList.remove('active');
document.body.style.cursor = '';
document.body.style.userSelect = '';
}
});
this._resizeEventBinded = true;
}
}
/**
* @description 从localStorage获取当前选择的配置
* @returns {Object} 包含plane、configurationId和domainId的对象
*/
getCurrentSelection() {
const selection = localStorage.getItem('xnsim-selection');
if (!selection) {
return { plane: '', configurationId: '', domainId: '' };
}
try {
const parsedSelection = JSON.parse(selection);
return {
plane: parsedSelection.plane || '',
configurationId: parsedSelection.configurationId || '',
domainId: parsedSelection.domainId || ''
};
} catch (error) {
return { plane: '', configurationId: '', domainId: '' };
}
}
/**
* @description 加载接口数据
*/
async loadInterfaces() {
try {
const { configurationId } = this.getCurrentSelection();
if (!configurationId) {
console.warn('未找到配置ID');
return;
}
const response = await fetch(`/api/interface/list?systemName=XNSim&confID=${configurationId}`);
const data = await response.json();
this.interfaces = data;
this.filteredInterfaces = this.filterInterfaces(this.searchText);
this.render();
} catch (error) {
console.error('加载接口数据失败:', error);
}
}
/**
* @description 根据搜索文本过滤接口
* @param {string} searchText - 搜索文本
* @returns {Array} 过滤后的接口数据
*/
filterInterfaces(searchText) {
if (!searchText) return this.interfaces;
return this.interfaces.filter(item =>
item.InterfaceName.toLowerCase().includes(searchText.toLowerCase()) ||
item.ModelStructName.toLowerCase().includes(searchText.toLowerCase())
);
}
/**
* @description 处理搜索输入
* @param {Event} event - 输入事件
*/
handleSearch(event) {
this.searchText = event.target.value;
this.cursorPosition = event.target.selectionStart;
// 清除之前的定时器
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
}
// 设置新的定时器300ms后执行搜索
this.searchTimeout = setTimeout(() => {
this.filteredInterfaces = this.filterInterfaces(this.searchText);
this.render();
// 在下一个事件循环中恢复焦点和光标位置
requestAnimationFrame(() => {
const searchInput = this.shadowRoot.querySelector('.search-input');
if (searchInput) {
searchInput.focus();
searchInput.setSelectionRange(this.cursorPosition, this.cursorPosition);
}
});
}, 300);
}
/**
* @description 处理树节点双击事件,将接口添加到表格
* @param {Object} item - 接口对象
*/
handleTreeItemDblClick(item) {
// 防止重复添加
if (!this.tableData.some(row => row.InterfaceName === item.InterfaceName && row.ModelStructName === item.ModelStructName)) {
this.tableData.push({
InterfaceName: item.InterfaceName,
ModelStructName: item.ModelStructName,
InjectValue: '',
Drawing: false,
isMonitoring: false,
color: this.getRandomColor()
});
this.renderTable();
}
}
/**
* @description 初始化DDS监控服务
* @returns {Promise<boolean>} 初始化是否成功
*/
async initializeDDSMonitor() {
try {
// 获取当前选择的配置
const selection = this.getCurrentSelection();
const { domainId } = selection;
if (!domainId) {
throw new Error('未找到有效的域ID请确保已选择构型并等待构型加载完成');
}
// 检查DDS监控状态
const statusResponse = await fetch('/api/dds-monitor/status');
if (!statusResponse.ok) {
throw new Error(`获取DDS监控状态失败: ${statusResponse.status} ${statusResponse.statusText}`);
}
const statusData = await statusResponse.json();
// 如果未初始化,则初始化
if (!statusData.isInitialized) {
const initResponse = await fetch('/api/dds-monitor/initialize', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
domainId,
monitorId: this.monitorId
})
});
if (!initResponse.ok) {
const errorData = await initResponse.json();
throw new Error(`初始化DDS监控失败: ${errorData.error || initResponse.statusText}`);
}
}
return true;
} catch (error) {
throw error;
}
}
/**
* @description 按结构体名称分组获取接口数据
* @returns {Object} 按结构体名称分组的接口数据
*/
getGroupedInterfaces() {
return this.tableData.reduce((groups, row) => {
if (!groups[row.ModelStructName]) {
groups[row.ModelStructName] = [];
}
groups[row.ModelStructName].push(row.InterfaceName);
return groups;
}, {});
}
/**
* @description 估算数据缓冲区大小
* @param {Array<string>} interfaceNames - 接口名称数组
* @returns {number} 估算的缓冲区大小(字节)
*/
estimateBufferSize(interfaceNames) {
// 基础开销JSON格式的开销括号、引号、逗号等
const baseOverhead = 100;
// 每个接口的估算大小
const interfaceSize = interfaceNames.reduce((total, name) => {
// 接口名称长度 + 引号 + 冒号
const nameOverhead = name.length + 4;
// 假设每个数据值平均长度为50字节包括数字、字符串等
const estimatedValueSize = 50;
// 逗号分隔符
const separatorSize = 1;
return total + nameOverhead + estimatedValueSize + separatorSize;
}, 0);
// 添加一些额外的缓冲空间20%
const safetyMargin = Math.ceil((baseOverhead + interfaceSize) * 0.2);
// 确保返回的大小是4KB的倍数并设置最小值为8KB
const minSize = 8192;
const size = Math.max(minSize, Math.ceil((baseOverhead + interfaceSize + safetyMargin) / 4096) * 4096);
return size;
}
/**
* @description 启动数据更新定时器
*/
startDataUpdateTimer() {
if (this.dataUpdateTimer) {
clearInterval(this.dataUpdateTimer);
}
this.dataUpdateTimer = setInterval(async () => {
try {
const groupedInterfaces = this.getGroupedInterfaces();
// 对每个结构体启动监控并获取数据
for (const [structName, interfaceNames] of Object.entries(groupedInterfaces)) {
try {
// 启动数据监控
const startResponse = await fetch('/api/data-monitor/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ structName })
});
if (!startResponse.ok) {
throw new Error(`启动数据监控失败: ${structName}`);
}
// 获取监控数据
const interfaceNamesStr = JSON.stringify(interfaceNames);
// 估算所需的缓冲区大小
const bufferSize = this.estimateBufferSize(interfaceNames);
const infoResponse = await fetch(`/api/data-monitor/info?structName=${encodeURIComponent(structName)}&interfaceName=${encodeURIComponent(interfaceNamesStr)}&bufferSize=${bufferSize}`);
if (!infoResponse.ok) {
throw new Error(`获取监控数据失败: ${structName}`);
}
const responseData = await infoResponse.json();
if (!responseData.success) {
throw new Error(`获取监控数据失败: ${responseData.message || '未知错误'}`);
}
if (!responseData.data) {
throw new Error(`获取监控数据失败: 返回数据为空`);
}
// 更新表格数据
this.tableData.forEach(row => {
if (row.ModelStructName === structName && responseData.data[row.InterfaceName]) {
row.monitorData = responseData.data[row.InterfaceName];
}
});
} catch (structError) {
console.error(`处理结构体 ${structName} 时出错:`, structError);
// 继续处理其他结构体
continue;
}
}
// 更新表格显示
this.renderTable();
} catch (error) {
console.error('数据更新失败:', error);
// 如果发生错误,停止定时器
this.stopDataUpdateTimer();
// 更新UI状态
const globalMonitorBtn = this.shadowRoot.getElementById('globalMonitorBtn');
const statusIndicator = this.shadowRoot.getElementById('statusIndicator');
const statusText = this.shadowRoot.getElementById('statusText');
if (globalMonitorBtn) {
globalMonitorBtn.textContent = '开始监控';
globalMonitorBtn.classList.remove('monitoring');
}
if (statusIndicator) {
statusIndicator.classList.remove('active');
statusIndicator.classList.add('error');
}
if (statusText) {
statusText.textContent = '监控错误';
}
// 清空所有监控数据
this.tableData.forEach(row => {
row.isMonitoring = false;
row.monitorData = '';
});
this.renderTable();
alert(`数据监控发生错误: ${error.message}`);
}
}, 1000); // 每秒更新一次
}
/**
* @description 停止数据更新定时器,并通知后端停止监控所有相关结构体
*/
async stopDataUpdateTimer() {
// 先清理前端定时器
if (this.dataUpdateTimer) {
clearInterval(this.dataUpdateTimer);
this.dataUpdateTimer = null;
}
// 收集所有正在监控的结构体名(去重)
const monitoringStructs = Array.from(new Set(
this.tableData
.filter(row => row.isMonitoring)
.map(row => row.ModelStructName)
));
// 并发调用后端接口,通知停止监控
try {
await Promise.all(
monitoringStructs.map(structName =>
fetch('/api/data-monitor/stop', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ structName })
})
)
);
} catch (error) {
console.error('停止后端监控时发生错误:', error);
}
}
/**
* @description 渲染表格内容
*/
renderTable() {
const tableBody = this.shadowRoot.querySelector('#monitor-table-body');
if (tableBody) {
tableBody.innerHTML = this.tableData.map((row, idx) => `
<tr>
<td><span class="cell-text">${row.InterfaceName}</span></td>
<td><span class="cell-text">${row.ModelStructName}</span></td>
<td><span class="cell-text">${row.monitorData || ''}</span></td>
<td>
<input type="text" class="inject-input" data-index="${idx}" value="${row.InjectValue || ''}" placeholder="注入值" style="width:90px;" />
</td>
<td style="min-width:120px; white-space:nowrap;">
<button class="action-btn chart-btn${row.Drawing ? ' drawing' : ''}" data-index="${idx}" ${!row.isMonitoring ? 'disabled' : ''}>绘图</button>
<button class="action-btn inject-once-btn" data-index="${idx}" ${!row.isMonitoring ? 'disabled' : ''}>单次注入</button>
<button class="action-btn inject-loop-btn" data-index="${idx}" ${!row.isMonitoring ? 'disabled' : ''}>连续注入</button>
<button class="action-btn delete-btn" data-index="${idx}">删除</button>
</td>
</tr>
`).join('');
}
// 注入值输入限制
this.shadowRoot.querySelectorAll('.inject-input').forEach(input => {
input.oninput = (e) => {
// 只允许数字、负号、小数点、逗号
let v = e.target.value.replace(/[^0-9\-.,]/g, '');
e.target.value = v;
const idx = parseInt(e.target.getAttribute('data-index'));
this.tableData[idx].InjectValue = v;
};
});
// 按钮事件
this.shadowRoot.querySelectorAll('.chart-btn').forEach(btn => {
btn.onclick = (e) => {
const idx = parseInt(btn.getAttribute('data-index'));
// 多选支持
if (this.tableData[idx].Drawing) {
this.tableData[idx].Drawing = false;
} else {
this.tableData[idx].Drawing = true;
}
// 更新activeChartIndexes
this.activeChartIndexes = this.tableData.map((row, i) => row.Drawing ? i : null).filter(i => i !== null);
this.renderTable();
this.updateChart();
};
});
this.shadowRoot.querySelectorAll('.delete-btn').forEach(btn => {
btn.onclick = (e) => {
const idx = parseInt(btn.getAttribute('data-index'));
this.tableData.splice(idx, 1);
// 删除后同步更新activeChartIndexes
this.activeChartIndexes = this.tableData.map((row, i) => row.Drawing ? i : null).filter(i => i !== null);
this.renderTable();
this.updateChart();
};
});
}
/**
* @description 初始化图表
*/
initChart() {
const chartElement = this.shadowRoot.querySelector('#monitor-chart');
if (!chartElement) return;
if (typeof Chart === 'undefined') return;
if (this.chart) {
this.chart.destroy();
this.chart = null;
}
const ctx = chartElement.getContext('2d');
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 }
}
}
});
}
/**
* @description 获取随机颜色
* @returns {string} 颜色字符串
*/
getRandomColor() {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
/**
* @description 更新图表,支持多条曲线
*/
updateChart() {
if (!this.chart) return;
if (!this.activeChartIndexes || this.activeChartIndexes.length === 0) {
this.chart.data.labels = [];
this.chart.data.datasets = [];
this.chart.update('none');
return;
}
// 多条曲线
const labels = Array.from({length: 20}, (_, i) => i+1);
this.chart.data.labels = labels;
this.chart.data.datasets = this.activeChartIndexes.map(idx => {
const row = this.tableData[idx];
return {
label: row.InterfaceName,
data: Array.from({length: 20}, () => (Math.random()*100).toFixed(2)),
borderColor: row.color,
fill: false
};
});
this.chart.update('none');
}
render() {
// 计算所有列宽之和
const totalColWidth = this.colWidths.reduce((a, b) => a + b, 0);
let tableWidthStyle = '';
const section = this.shadowRoot && this.shadowRoot.querySelector('.table-section');
const containerWidth = section ? section.clientWidth : 940;
if (totalColWidth < containerWidth) {
tableWidthStyle = 'width:100%;';
} else {
tableWidthStyle = 'width:max-content;';
}
// 首次渲染时动态分配列宽已移至connectedCallback防止死循环
// 按ModelStructName分组
const groupedInterfaces = this.filteredInterfaces.reduce((groups, item) => {
const group = groups[item.ModelStructName] || [];
group.push(item);
groups[item.ModelStructName] = group;
return groups;
}, {});
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
height: 100%;
overflow: auto;
padding: 16px;
box-sizing: border-box;
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
margin-bottom: 16px;
}
.toolbar-left {
display: flex;
align-items: center;
gap: 12px;
}
.toolbar-right {
display: flex;
align-items: center;
gap: 12px;
}
.monitor-status {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #666;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: #ccc;
}
.status-indicator.active {
background: #52c41a;
}
.status-indicator.error {
background: #ff4d4f;
}
.global-monitor-btn {
padding: 6px 16px;
border: none;
border-radius: 4px;
background: #1890ff;
color: white;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.global-monitor-btn:hover {
background: #40a9ff;
}
.global-monitor-btn.monitoring {
background: #ff4d4f;
}
.global-monitor-btn.monitoring:hover {
background: #ff7875;
}
.monitor-container {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 16px;
height: calc(100% - 72px);
box-sizing: border-box;
display: flex;
gap: 16px;
}
.tree-container {
width: 300px;
border-right: 1px solid #e0e0e0;
padding-right: 16px;
display: flex;
flex-direction: column;
}
.search-box {
margin-bottom: 16px;
}
.search-input {
width: 100%;
padding: 8px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
}
.tree-view {
flex-grow: 1;
overflow-y: auto;
}
.tree-group {
margin-bottom: 8px;
}
.group-header {
font-weight: bold;
padding: 8px;
background-color: #f5f5f5;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
}
.group-content {
margin-left: 20px;
display: block;
}
.group-content.collapsed {
display: none;
}
.interface-item {
padding: 6px 8px;
cursor: pointer;
border-radius: 4px;
margin: 2px 0;
user-select: none;
}
.interface-item:hover {
background-color: #f0f0f0;
}
.content-area {
flex-grow: 1;
display: flex;
flex-direction: column;
height: 100%;
min-width: 0;
min-height: 0;
overflow: hidden;
}
.table-section {
background: #fff;
border-radius: 8px 8px 0 0;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
padding: 0;
margin-bottom: 0;
overflow: hidden;
max-height: 70vh;
min-height: 60px;
height: ${this.tableHeight}px;
transition: height 0.1s;
display: flex;
flex-direction: column;
}
.table-scroll-x {
width: 100%;
height: 100%;
overflow-x: auto;
overflow-y: hidden;
display: flex;
flex-direction: column;
}
.table-header-fixed {
overflow: hidden;
width: fit-content;
}
.table-header-fixed .monitor-table {
width: max-content;
min-width: 100%;
}
.table-body-scroll {
flex: 1;
overflow-y: auto;
width: 100%;
overflow-x: hidden;
overflow-x: auto;
max-height: calc(100% - 1px);
}
.table-body-scroll .monitor-table {
width: max-content;
min-width: 100%;
}
.monitor-table {
width: max-content;
min-width: 100%;
border-collapse: separate;
border-spacing: 0;
table-layout: fixed;
min-width: unset;
}
.monitor-table th, .monitor-table td {
padding: 8px;
border-bottom: 1px solid #e0e0e0;
text-align: left;
background: #fff;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
position: relative;
max-width: 0;
}
.cell-text {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
.monitor-table input,
.monitor-table button {
min-width: 0;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.monitor-table th {
background: #f5f5f5;
position: sticky;
top: 0;
z-index: 2;
position: relative;
}
/* 竖线:表头和内容区 */
.monitor-table td:not(:last-child)::after {
content: '';
position: absolute;
right: 0;
top: 20%;
width: 1px;
height: 60%;
background: #e0e0e0;
z-index: 1;
}
.monitor-table th.th-resize-active::after {
background: #1890ff;
}
.divider {
height: 12px;
background: transparent;
cursor: row-resize;
width: 100%;
position: relative;
z-index: 2;
transition: background 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.divider-bar {
width: 60px;
height: 5px;
background: #c0c4cc;
border-radius: 3px;
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
transition: background 0.2s, box-shadow 0.2s;
}
.divider:hover .divider-bar,
.divider.active .divider-bar {
background: #1890ff;
box-shadow: 0 2px 8px rgba(24,144,255,0.15);
}
.chart-section {
background: #fff;
border-radius: 0 0 8px 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
padding: 12px;
flex: 1;
display: flex;
flex-direction: column;
min-height: 120px;
min-width: 0;
overflow: hidden;
}
.chart-title {
font-size: 15px;
font-weight: bold;
margin-bottom: 8px;
}
.chart-container {
flex: 1;
min-height: 120px;
position: relative;
}
#monitor-chart {
width: 100% !important;
height: 100% !important;
display: block;
}
.action-btn {
margin: 0 2px;
padding: 3px 8px;
border: none;
border-radius: 4px;
background: #f0f0f0;
color: #333;
font-size: 13px;
cursor: pointer;
transition: background 0.2s;
}
.action-btn:hover {
background: #e6f7ff;
}
.chart-btn.drawing {
background: #1890ff;
color: #fff;
}
.th-resize {
position: absolute;
right: 0;
top: 0;
width: 12px;
height: 100%;
cursor: col-resize;
z-index: 10;
user-select: none;
display: flex;
align-items: center;
justify-content: center;
}
.th-resize::after {
content: '';
position: absolute;
right: 0;
top: 20%;
width: 1px;
height: 60%;
background: #e0e0e0;
border-radius: 2px;
transition: background 0.2s;
}
.th-resize:hover::after,
.th-resize.active::after {
background: #1890ff;
}
.monitor-btn.monitoring {
background: #ff4d4f;
color: #fff;
}
.action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
<div class="toolbar">
<div class="toolbar-left">
<button class="global-monitor-btn" id="globalMonitorBtn">开始监控</button>
</div>
<div class="toolbar-right">
<div class="monitor-status">
<div class="status-indicator" id="statusIndicator"></div>
<span id="statusText">未监控</span>
</div>
</div>
</div>
<div class="monitor-container">
<div class="tree-container">
<div class="search-box">
<input type="text"
class="search-input"
placeholder="搜索接口..."
value="${this.searchText}">
</div>
<div class="tree-view">
${Object.entries(groupedInterfaces).map(([groupName, items]) => `
<div class="tree-group">
<div class="group-header" onclick="this.parentElement.querySelector('.group-content').classList.toggle('collapsed')">
<span class="group-icon">▼</span>
${groupName}
</div>
<div class="group-content">
${items.map(item => `
<div class="interface-item" data-interfacename="${item.InterfaceName}" data-modelstructname="${item.ModelStructName}">
${item.InterfaceName}
</div>
`).join('')}
</div>
</div>
`).join('')}
</div>
</div>
<div class="content-area">
<div class="table-section">
<div class="table-header-fixed">
<table class="monitor-table" style="${tableWidthStyle}">
<colgroup>
<col style="width:${this.colWidths[0]}px">
<col style="width:${this.colWidths[1]}px">
<col style="width:${this.colWidths[2]}px">
<col style="width:${this.colWidths[3]}px">
<col style="width:${this.colWidths[4]}px">
</colgroup>
<thead>
<tr>
<th>接口名称<div class="th-resize" data-col="0"></div></th>
<th>结构体名<div class="th-resize" data-col="1"></div></th>
<th>数据<div class="th-resize" data-col="2"></div></th>
<th>注入值<div class="th-resize" data-col="3"></div></th>
<th>操作<div class="th-resize" data-col="4"></div></th>
</tr>
</thead>
</table>
</div>
<div class="table-body-scroll">
<table class="monitor-table" style="${tableWidthStyle}">
<colgroup>
<col style="width:${this.colWidths[0]}px">
<col style="width:${this.colWidths[1]}px">
<col style="width:${this.colWidths[2]}px">
<col style="width:${this.colWidths[3]}px">
<col style="width:${this.colWidths[4]}px">
</colgroup>
<tbody id="monitor-table-body">
<!-- 表格内容 -->
</tbody>
</table>
</div>
</div>
<div class="divider"><div class="divider-bar"></div></div>
<div class="chart-section">
<div class="chart-title">数据监控图表</div>
<div class="chart-container">
<canvas id="monitor-chart"></canvas>
</div>
</div>
</div>
</div>
`;
// 搜索框事件
const searchInput = this.shadowRoot.querySelector('.search-input');
if (searchInput) {
searchInput.addEventListener('input', (e) => this.handleSearch(e));
}
// 树节点双击事件
this.shadowRoot.querySelectorAll('.interface-item').forEach(itemEl => {
itemEl.ondblclick = (e) => {
const name = itemEl.getAttribute('data-interfacename');
const struct = itemEl.getAttribute('data-modelstructname');
this.handleTreeItemDblClick({ InterfaceName: name, ModelStructName: struct });
};
});
// 渲染表格内容
this.renderTable();
// 初始化图表
this.initChart();
// 初始绘图
this.updateChart();
// 分隔线拖动事件
const divider = this.shadowRoot.querySelector('.divider');
if (divider) {
divider.onmousedown = (e) => {
this.isResizing = true;
this.startY = e.clientY;
this.startTableHeight = this.tableHeight;
divider.classList.add('active');
document.body.style.cursor = 'row-resize';
document.body.style.userSelect = 'none';
};
}
// 列宽拖动事件
this.shadowRoot.querySelectorAll('.th-resize').forEach(handle => {
handle.onmousedown = (e) => {
e.preventDefault();
this.dragColIndex = parseInt(handle.getAttribute('data-col'));
this.dragStartX = e.clientX;
this.dragStartWidth = this.colWidths[this.dragColIndex];
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
handle.classList.add('active');
// 高亮竖线
const th = handle.parentElement;
if (th) th.classList.add('th-resize-active');
};
handle.onmouseup = () => {
handle.classList.remove('active');
const th = handle.parentElement;
if (th) th.classList.remove('th-resize-active');
};
});
window.onmousemove = (e) => {
if (this.dragColIndex !== null) {
const delta = e.clientX - this.dragStartX;
let newWidth = this.dragStartWidth + delta;
if (newWidth < 60) newWidth = 60;
this.colWidths[this.dragColIndex] = newWidth;
this.render();
}
};
window.onmouseup = () => {
if (this.dragColIndex !== null) {
// 移除所有th-resize的active
this.shadowRoot.querySelectorAll('.th-resize').forEach(h => h.classList.remove('active'));
// 移除所有th的高亮
this.shadowRoot.querySelectorAll('.monitor-table th').forEach(th => th.classList.remove('th-resize-active'));
this.dragColIndex = null;
document.body.style.cursor = '';
document.body.style.userSelect = '';
}
};
// 绑定全局监控按钮事件
const globalMonitorBtn = this.shadowRoot.getElementById('globalMonitorBtn');
const statusIndicator = this.shadowRoot.getElementById('statusIndicator');
const statusText = this.shadowRoot.getElementById('statusText');
globalMonitorBtn.addEventListener('click', async () => {
try {
if (!globalMonitorBtn.classList.contains('monitoring')) {
// 开始监控
const ddsInitialized = await this.initializeDDSMonitor();
if (!ddsInitialized) {
throw new Error('DDS监控初始化失败');
}
// 更新所有行的监控状态
this.tableData.forEach(row => {
row.isMonitoring = true;
});
// 启动数据更新定时器
this.startDataUpdateTimer();
// 更新状态
globalMonitorBtn.textContent = '停止监控';
globalMonitorBtn.classList.add('monitoring');
statusIndicator.classList.add('active');
statusText.textContent = '监控中';
this.renderTable();
} else {
// 停止监控
// 停止数据更新定时器
await this.stopDataUpdateTimer();
// 更新所有行的监控状态
this.tableData.forEach(row => {
row.isMonitoring = false;
row.monitorData = ''; // 清空监控数据
});
// 更新状态
globalMonitorBtn.textContent = '开始监控';
globalMonitorBtn.classList.remove('monitoring');
statusIndicator.classList.remove('active');
statusText.textContent = '未监控';
this.renderTable();
}
} catch (error) {
statusIndicator.classList.add('error');
statusText.textContent = '监控错误';
alert(error.message);
}
});
}
disconnectedCallback() {
// 组件销毁时清理定时器
this.stopDataUpdateTimer();
}
}
customElements.define('data-monitor', DataMonitor);