XNSim/XNSimHtml/components/data-monitor.js

1150 lines
44 KiB
JavaScript
Raw Normal View History

/**
* @class DataMonitor
* @extends HTMLElement
* @description 数据监控组件的基础类
*/
2025-04-28 12:25:20 +08:00
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; // 数据更新定时器
2025-04-28 12:25:20 +08:00
}
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);
2025-04-28 16:41:21 +08:00
}
};
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;
2025-04-28 16:41:21 +08:00
}
}
/**
* @description 从localStorage获取当前选择的配置
* @returns {Object} 包含planeconfigurationId和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: '' };
}
2025-04-28 16:41:21 +08:00
}
/**
* @description 加载接口数据
*/
async loadInterfaces() {
2025-04-28 16:41:21 +08:00
try {
const { configurationId } = this.getCurrentSelection();
if (!configurationId) {
console.warn('未找到配置ID');
2025-04-28 16:41:21 +08:00
return;
}
const response = await fetch(`/api/interface/list?systemName=XNSim&confID=${configurationId}`);
2025-04-28 16:41:21 +08:00
const data = await response.json();
this.interfaces = data;
this.filteredInterfaces = this.filterInterfaces(this.searchText);
this.render();
2025-04-28 16:41:21 +08:00
} catch (error) {
console.error('加载接口数据失败:', error);
2025-04-28 16:41:21 +08:00
}
}
/**
* @description 根据搜索文本过滤接口
* @param {string} searchText - 搜索文本
* @returns {Array} 过滤后的接口数据
*/
filterInterfaces(searchText) {
if (!searchText) return this.interfaces;
2025-04-28 16:41:21 +08:00
return this.interfaces.filter(item =>
item.InterfaceName.toLowerCase().includes(searchText.toLowerCase()) ||
item.ModelStructName.toLowerCase().includes(searchText.toLowerCase())
);
2025-04-28 16:41:21 +08:00
}
/**
* @description 处理搜索输入
* @param {Event} event - 输入事件
*/
handleSearch(event) {
this.searchText = event.target.value;
this.cursorPosition = event.target.selectionStart;
2025-04-28 16:41:21 +08:00
// 清除之前的定时器
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
}
2025-04-28 16:41:21 +08:00
// 设置新的定时器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);
2025-04-28 16:41:21 +08:00
}
/**
* @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();
2025-04-28 16:41:21 +08:00
}
}
/**
* @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('');
2025-04-28 16:41:21 +08:00
}
// 注入值输入限制
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;
2025-04-28 16:41:21 +08:00
}
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 }
}
}
});
2025-04-28 16:41:21 +08:00
}
/**
* @description 获取随机颜色
* @returns {string} 颜色字符串
*/
getRandomColor() {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
2025-04-28 16:41:21 +08:00
}
return color;
2025-04-28 16:41:21 +08:00
}
/**
* @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;
2025-04-28 16:41:21 +08:00
}
// 多条曲线
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');
2025-04-28 16:41:21 +08:00
}
2025-04-28 12:25:20 +08:00
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;
}, {});
2025-04-28 12:25:20 +08:00
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;
}
2025-04-28 12:25:20 +08:00
.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);
2025-04-28 12:25:20 +08:00
box-sizing: border-box;
2025-04-28 16:41:21 +08:00
display: flex;
gap: 16px;
2025-04-28 12:25:20 +08:00
}
.tree-container {
width: 300px;
border-right: 1px solid #e0e0e0;
padding-right: 16px;
2025-04-28 12:25:20 +08:00
display: flex;
flex-direction: column;
2025-04-28 12:25:20 +08:00
}
.search-box {
margin-bottom: 16px;
}
.search-input {
width: 100%;
padding: 8px;
border: 1px solid #d9d9d9;
2025-04-28 16:41:21 +08:00
border-radius: 4px;
font-size: 14px;
}
.tree-view {
flex-grow: 1;
overflow-y: auto;
}
.tree-group {
margin-bottom: 8px;
}
.group-header {
2025-04-28 16:41:21 +08:00
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;
2025-04-28 16:41:21 +08:00
}
.group-content.collapsed {
display: none;
}
.interface-item {
padding: 6px 8px;
cursor: pointer;
border-radius: 4px;
margin: 2px 0;
user-select: none;
}
.interface-item:hover {
2025-04-28 16:41:21 +08:00
background-color: #f0f0f0;
}
.content-area {
2025-04-28 16:41:21 +08:00
flex-grow: 1;
display: flex;
flex-direction: column;
height: 100%;
min-width: 0;
min-height: 0;
overflow: hidden;
2025-04-28 16:41:21 +08:00
}
.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;
2025-04-28 16:41:21 +08:00
display: flex;
flex-direction: column;
}
.table-scroll-x {
width: 100%;
height: 100%;
overflow-x: auto;
overflow-y: hidden;
display: flex;
flex-direction: column;
2025-04-28 16:41:21 +08:00
}
.table-header-fixed {
overflow: hidden;
width: fit-content;
2025-04-28 16:41:21 +08:00
}
.table-header-fixed .monitor-table {
width: max-content;
min-width: 100%;
2025-04-28 16:41:21 +08:00
}
.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;
2025-04-28 16:41:21 +08:00
}
.cell-text {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
2025-04-28 16:41:21 +08:00
white-space: nowrap;
vertical-align: middle;
2025-04-28 16:41:21 +08:00
}
.monitor-table input,
.monitor-table button {
min-width: 0;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
2025-04-28 16:41:21 +08:00
}
.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;
2025-04-28 16:41:21 +08:00
}
.monitor-table th.th-resize-active::after {
background: #1890ff;
2025-04-28 16:41:21 +08:00
}
.divider {
height: 12px;
background: transparent;
cursor: row-resize;
width: 100%;
position: relative;
z-index: 2;
transition: background 0.2s;
2025-04-28 16:41:21 +08:00
display: flex;
align-items: center;
justify-content: center;
2025-04-28 16:41:21 +08:00
}
.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;
2025-04-28 16:41:21 +08:00
}
.divider:hover .divider-bar,
.divider.active .divider-bar {
background: #1890ff;
box-shadow: 0 2px 8px rgba(24,144,255,0.15);
2025-04-28 16:41:21 +08:00
}
.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;
2025-04-28 16:41:21 +08:00
display: flex;
flex-direction: column;
min-height: 120px;
min-width: 0;
overflow: hidden;
2025-04-28 16:41:21 +08:00
}
.chart-title {
font-size: 15px;
font-weight: bold;
margin-bottom: 8px;
2025-04-28 16:41:21 +08:00
}
.chart-container {
flex: 1;
min-height: 120px;
position: relative;
2025-04-28 16:41:21 +08:00
}
#monitor-chart {
width: 100% !important;
height: 100% !important;
display: block;
2025-04-28 16:41:21 +08:00
}
.action-btn {
margin: 0 2px;
padding: 3px 8px;
border: none;
border-radius: 4px;
background: #f0f0f0;
2025-04-28 12:25:20 +08:00
color: #333;
font-size: 13px;
cursor: pointer;
transition: background 0.2s;
2025-04-28 16:41:21 +08:00
}
.action-btn:hover {
background: #e6f7ff;
2025-04-28 16:41:21 +08:00
}
.chart-btn.drawing {
background: #1890ff;
color: #fff;
2025-04-28 16:41:21 +08:00
}
.th-resize {
position: absolute;
right: 0;
top: 0;
width: 12px;
height: 100%;
cursor: col-resize;
z-index: 10;
user-select: none;
2025-04-28 16:41:21 +08:00
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;
2025-04-28 12:25:20 +08:00
}
.monitor-btn.monitoring {
background: #ff4d4f;
color: #fff;
}
.action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
2025-04-28 12:25:20 +08:00
</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>
2025-04-28 12:25:20 +08:00
<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>
2025-04-28 16:41:21 +08:00
</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>
2025-04-28 12:25:20 +08:00
</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();
2025-04-28 16:41:21 +08:00
}
};
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 = '';
2025-04-28 16:41:21 +08:00
}
};
// 绑定全局监控按钮事件
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();
2025-04-28 12:25:20 +08:00
}
}
customElements.define('data-monitor', DataMonitor);