XNSim/XNSimHtml/components/data-monitor.js

1642 lines
64 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.monitorId = `data_monitor_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; // 添加监控器ID
this.dataUpdateTimer = null; // 数据更新定时器
this.isActive = false; // 组件是否激活
this.chartWindows = new Map(); // 存储打开的图表窗口
// 绑定方法
this.handleSearch = this.handleSearch.bind(this);
this.handleTreeItemDblClick = this.handleTreeItemDblClick.bind(this);
this.handlePlot = this.handlePlot.bind(this);
this.handleDelete = this.handleDelete.bind(this);
// 确保 FloatingChartWindow 组件已注册
if (!customElements.get('floating-chart-window')) {
const script = document.createElement('script');
script.src = './components/FloatingChartWindow.js';
document.head.appendChild(script);
}
}
connectedCallback() {
this.isActive = true; // 设置初始状态为激活
this.loadInterfaces();
// 等待组件完全加载后初始化
setTimeout(() => {
this.initializeComponent();
// 初始化完成后再启动定时器,给服务器一些准备时间
setTimeout(() => {
this.startDataUpdateTimer();
}, 1000); // 延迟1秒启动定时器
}, 100);
}
// 重新激活组件
reactivate() {
if (this.isActive) return; // 如果已经激活,直接返回
this.isActive = true;
this.initializeComponent();
// 重新启动定时器
this.startDataUpdateTimer();
}
async initializeComponent() {
try {
// 更新所有行的监控状态
this.tableData.forEach(row => {
row.isMonitoring = true;
});
// 初始状态设置为未监控
const statusIndicator = this.shadowRoot.getElementById('statusIndicator');
const statusText = this.shadowRoot.getElementById('statusText');
if (statusIndicator) {
statusIndicator.classList.remove('active', 'error');
statusIndicator.classList.add('inactive');
}
if (statusText) {
statusText.textContent = '未监控';
}
} catch (error) {
console.error('初始化组件失败:', error);
const statusIndicator = this.shadowRoot.getElementById('statusIndicator');
const statusText = this.shadowRoot.getElementById('statusText');
if (statusIndicator) {
statusIndicator.classList.remove('active', 'inactive');
statusIndicator.classList.add('error');
}
if (statusText) {
statusText.textContent = '监控错误';
}
}
}
/**
* @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: '',
InjectFrequency: 100,
monitorData: '',
isMonitoring: false,
isInjecting: false
});
// 创建新行
const tbody = this.shadowRoot.querySelector('.data-table tbody');
const tr = document.createElement('tr');
tr.setAttribute('data-interface', item.InterfaceName);
tr.setAttribute('data-struct', item.ModelStructName);
tr.innerHTML = `
<td title="${item.InterfaceName}">${item.InterfaceName}</td>
<td title="${item.ModelStructName}">${item.ModelStructName}</td>
<td class="data-cell" title="-">-</td>
<td>
<input type="text"
class="inject-input"
value=""
placeholder="输入注入值"
data-interface="${item.InterfaceName}"
data-struct="${item.ModelStructName}">
</td>
<td>
<input type="number"
class="frequency-input"
value="100"
min="10"
max="10000"
step="10"
placeholder="输入频率"
data-interface="${item.InterfaceName}"
data-struct="${item.ModelStructName}">
</td>
<td>
<div class="action-buttons">
<button class="action-button plot" data-interface="${item.InterfaceName}" data-struct="${item.ModelStructName}">绘图</button>
<button class="action-button inject-once" data-interface="${item.InterfaceName}" data-struct="${item.ModelStructName}">注入一次</button>
<button class="action-button inject-continuous" data-interface="${item.InterfaceName}" data-struct="${item.ModelStructName}">连续注入</button>
<button class="action-button delete" data-interface="${item.InterfaceName}" data-struct="${item.ModelStructName}">删除</button>
</div>
</td>
`;
tbody.appendChild(tr);
}
}
/**
* @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 格式化监控数据
* @param {string} data - 原始数据
* @returns {string} 格式化后的数据
*/
formatMonitorData(data) {
if (!data) return '-';
try {
// 尝试解析JSON数据
const parsedData = JSON.parse(data);
// 如果是对象,转换为格式化的字符串
if (typeof parsedData === 'object') {
return JSON.stringify(parsedData, null, 2);
}
return data;
} catch (e) {
// 如果不是JSON直接返回原始数据
return data;
}
}
/**
* @description 更新表格数据
* @param {Object} newData - 新的监控数据
*/
updateTableData(newData) {
let hasChanges = false;
// 更新内存中的数据
this.tableData.forEach(row => {
const newValue = newData[row.InterfaceName];
if (newValue !== undefined && newValue !== row.monitorData) {
row.monitorData = newValue;
hasChanges = true;
}
});
if (hasChanges) {
// 只更新数据列的内容
const rows = this.shadowRoot.querySelectorAll('.data-table tbody tr');
rows.forEach((row, rowIndex) => {
const dataCell = row.querySelector('td:nth-child(3)');
if (dataCell) {
const formattedData = this.formatMonitorData(this.tableData[rowIndex].monitorData);
dataCell.textContent = formattedData;
dataCell.title = formattedData;
}
});
}
}
/**
* @description 启动数据更新定时器
*/
startDataUpdateTimer() {
if (this.dataUpdateTimer) {
clearInterval(this.dataUpdateTimer);
}
this.dataUpdateTimer = setInterval(async () => {
try {
// 检查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();
// 更新状态显示
const statusIndicator = this.shadowRoot.getElementById('statusIndicator');
const statusText = this.shadowRoot.getElementById('statusText');
if (!statusData.isInitialized) {
if (statusIndicator) {
statusIndicator.classList.remove('active');
statusIndicator.classList.add('inactive');
}
if (statusText) {
statusText.textContent = '未监控';
}
// 更新表格中所有数据的监控状态
this.tableData.forEach(row => {
row.isMonitoring = false;
row.monitorData = '';
});
// 只更新数据单元格
const dataCells = this.shadowRoot.querySelectorAll('.data-cell');
dataCells.forEach(cell => {
cell.textContent = '-';
cell.title = '-';
});
return; // 未初始化时等待下一次循环
}
// DDS已初始化更新状态显示
if (statusIndicator) {
statusIndicator.classList.remove('inactive', 'error');
statusIndicator.classList.add('active');
}
if (statusText) {
statusText.textContent = '监控中';
}
const groupedInterfaces = this.getGroupedInterfaces();
const allData = {}; // 存储所有结构体的数据
// 对每个结构体启动监控并获取数据
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(`获取监控数据失败: 返回数据为空`);
}
// 合并数据
Object.assign(allData, responseData.data);
} catch (structError) {
console.error(`处理结构体 ${structName} 时出错:`, structError);
continue;
}
}
// 一次性更新所有数据
if (Object.keys(allData).length > 0) {
this.updateTableData(allData);
}
} catch (error) {
console.error('数据更新失败:', error);
this.stopDataUpdateTimer();
const statusIndicator = this.shadowRoot.getElementById('statusIndicator');
const statusText = this.shadowRoot.getElementById('statusText');
if (statusIndicator) {
statusIndicator.classList.remove('active');
statusIndicator.classList.add('error');
}
if (statusText) {
statusText.textContent = '监控错误';
}
this.tableData.forEach(row => {
row.isMonitoring = false;
row.monitorData = '';
});
// 只更新数据单元格
const dataCells = this.shadowRoot.querySelectorAll('.data-cell');
dataCells.forEach(cell => {
cell.textContent = '-';
cell.title = '-';
});
}
}, 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 获取随机颜色
* @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 处理绘图按钮点击事件
* @param {string} interfaceName - 接口名称
* @param {string} modelStructName - 结构体名称
*/
async handlePlot(interfaceName, modelStructName) {
// 检查监控状态
const statusIndicator = this.shadowRoot.getElementById('statusIndicator');
if (!statusIndicator || !statusIndicator.classList.contains('active')) {
return; // 如果不在监控状态,直接返回
}
// 检查是否已经存在该接口的图表窗口
const windowId = `${interfaceName}_${modelStructName}`;
if (this.chartWindows.has(windowId)) {
// 如果窗口已存在,将其置顶
const window = this.chartWindows.get(windowId);
window.setAttribute('z-index', Date.now());
return;
}
if (typeof Chart === 'undefined') {
console.log('Chart.js 未加载...');
}
try {
// 创建图表数据
const chartData = {
labels: [],
datasets: []
};
// 创建图表配置
const chartOptions = {
responsive: true,
maintainAspectRatio: false,
animation: false,
elements: {
point: {
radius: 0
},
line: {
tension: 0
}
},
scales: {
y: {
beginAtZero: false,
display: true,
ticks: {
callback: function(value) {
// 如果数值的绝对值大于1000或小于0.1,使用科学计数法
if (Math.abs(value) > 1000 || (Math.abs(value) < 0.1 && value !== 0)) {
return value.toExponential(1);
}
// 否则使用普通数字,保留一位小数
return value.toFixed(1);
},
maxTicksLimit: 8 // 限制刻度数量
}
},
x: {
display: true,
ticks: {
callback: function(value) {
// 如果数值大于1000使用科学计数法
if (value > 1000) {
return value.toExponential(1);
}
return value;
}
}
}
},
plugins: {
legend: {
display: true,
position: 'top'
},
tooltip: {
enabled: true,
mode: 'index',
intersect: false,
callbacks: {
label: function(context) {
const value = context.raw;
// 始终使用普通数字格式,保留一位小数
return `${context.dataset.label}: ${value.toFixed(1)}`;
}
}
}
}
};
// 创建浮动窗口组件
const floatingWindow = document.createElement('floating-chart-window');
if (!floatingWindow) {
throw new Error('创建浮动窗口组件失败');
}
// 添加数据点计数器
floatingWindow.dataPointIndex = 0;
// 先添加到页面
document.body.appendChild(floatingWindow);
this.chartWindows.set(windowId, floatingWindow);
// 再设置属性
floatingWindow.setAttribute('title', interfaceName);
floatingWindow.setAttribute('initial-position', JSON.stringify({ x: 400, y: 200 }));
floatingWindow.setAttribute('initial-size', JSON.stringify({ width: 400, height: 300 }));
floatingWindow.setAttribute('chart-data', JSON.stringify(chartData));
floatingWindow.setAttribute('chart-options', JSON.stringify(chartOptions));
// 更新图表数据
const updateChartData = () => {
const row = this.tableData.find(r =>
r.InterfaceName === interfaceName &&
r.ModelStructName === modelStructName
);
if (row && row.monitorData) {
try {
const data = row.monitorData;
let values = [];
// 尝试解析数据
if (typeof data === 'string' && data.includes(',')) {
// 如果是逗号分隔的字符串,分割并转换为数字
values = data.split(',').map(v => parseFloat(v.trim()));
} else if (typeof data === 'number') {
// 如果是单个数字
values = [data];
} else {
// 尝试将字符串转换为数字
const numValue = parseFloat(data);
values = isNaN(numValue) ? [] : [numValue];
}
// 如果数据有效,更新图表
if (values.length > 0) {
floatingWindow.handleDataUpdate(values, interfaceName);
}
} catch (e) {
console.error('解析数据失败:', e);
}
}
};
// 将更新函数添加到数据更新定时器中
const originalUpdateTableData = this.updateTableData;
this.updateTableData = (newData) => {
originalUpdateTableData.call(this, newData);
updateChartData();
};
// 添加关闭事件处理
floatingWindow.addEventListener('close', () => {
// 恢复原始的 updateTableData 方法
this.updateTableData = originalUpdateTableData;
// 从 Map 中移除窗口引用
this.chartWindows.delete(windowId);
});
} catch (error) {
console.error('创建图表窗口失败:', error);
alert('创建图表窗口失败,请查看控制台了解详情');
}
}
render() {
// 获取当前监控状态
const statusIndicator = this.shadowRoot.getElementById('statusIndicator');
const isMonitoring = statusIndicator && statusIndicator.classList.contains('active');
// 按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;
}
.status-indicator.inactive {
background: #d9d9d9;
}
.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-container {
flex: 1;
overflow: auto;
margin-top: 16px;
width: 100%;
position: relative;
}
.data-table {
width: 100%;
border-collapse: collapse;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
table-layout: fixed;
}
.data-table th,
.data-table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid #f0f0f0;
border-right: 1px solid #f0f0f0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
position: relative;
}
.data-table th:last-child,
.data-table td:last-child {
border-right: none;
width: 300px !important; /* 固定操作列宽度 */
}
.data-table th:last-child .resizer {
display: none; /* 隐藏操作列的调整器 */
}
.data-table th {
background: #fafafa;
font-weight: 500;
color: #262626;
user-select: none;
}
.data-table tr:hover {
background: #fafafa;
}
.resizer {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 5px;
cursor: col-resize;
background: transparent;
}
.resizer:hover,
.resizer.active {
background: #1890ff;
}
.action-buttons {
display: flex;
gap: 8px;
}
.action-button {
padding: 4px 8px;
border: 1px solid #d9d9d9;
border-radius: 4px;
background: white;
cursor: pointer;
font-size: 12px;
color: #262626;
transition: all 0.3s;
}
.action-button:hover {
color: #1890ff;
border-color: #1890ff;
}
.action-button.delete {
color: #ff4d4f;
}
.action-button.delete:hover {
border-color: #ff4d4f;
}
.data-cell {
font-family: monospace;
white-space: pre-wrap;
word-break: break-all;
max-height: 100px;
overflow-y: auto;
}
.inject-input {
width: 100%;
padding: 4px 8px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
.inject-input:focus {
border-color: #1890ff;
outline: none;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.inject-input.invalid {
border-color: #ff4d4f;
}
.inject-input.invalid:focus {
box-shadow: 0 0 0 2px rgba(255, 77, 79, 0.2);
}
.frequency-input {
width: 100%;
padding: 4px 8px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
.frequency-input:focus {
border-color: #1890ff;
outline: none;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.frequency-input:disabled {
background-color: #f5f5f5;
cursor: not-allowed;
}
.action-button:disabled {
background-color: #f5f5f5;
color: #d9d9d9;
border-color: #d9d9d9;
cursor: not-allowed;
}
.floating-chart-window {
position: fixed;
width: 400px;
height: 300px;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
overflow: hidden;
left: 100px;
top: 100px;
}
.window-header {
background: #f5f5f5;
padding: 8px 12px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: move;
user-select: none;
border-bottom: 1px solid #e8e8e8;
}
.window-title {
font-size: 14px;
font-weight: 500;
color: #262626;
}
.window-controls {
display: flex;
gap: 8px;
}
.window-control-button {
width: 24px;
height: 24px;
border: none;
background: transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #666;
border-radius: 4px;
transition: all 0.3s;
}
.window-control-button:hover {
background: rgba(0, 0, 0, 0.05);
color: #262626;
}
.window-control-button.close-button:hover {
background: #ff4d4f;
color: white;
}
.window-content {
flex: 1;
padding: 12px;
position: relative;
}
.window-content canvas {
width: 100% !important;
height: 100% !important;
}
.action-button.active {
background-color: #ff4d4f;
color: white;
border-color: #ff4d4f;
}
.action-button.active:hover {
background-color: #ff7875;
border-color: #ff7875;
}
.inject-input:disabled {
background-color: #f5f5f5;
cursor: not-allowed;
}
</style>
<div class="toolbar">
<div class="toolbar-left">
<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-container">
<table class="data-table">
<thead>
<tr>
<th style="width: 200px">接口名<div class="resizer"></div></th>
<th style="width: 200px">结构体名<div class="resizer"></div></th>
<th style="width: 200px">数据<div class="resizer"></div></th>
<th style="width: 200px">注入值<div class="resizer"></div></th>
<th style="width: 120px">注入频率(Hz)<div class="resizer"></div></th>
<th>操作</th>
</tr>
</thead>
<tbody>
${this.tableData.map(row => {
return `
<tr data-interface="${row.InterfaceName}" data-struct="${row.ModelStructName}">
<td title="${row.InterfaceName}">${row.InterfaceName}</td>
<td title="${row.ModelStructName}">${row.ModelStructName}</td>
<td class="data-cell" title="${this.formatMonitorData(row.monitorData)}">${this.formatMonitorData(row.monitorData)}</td>
<td>
<input type="text"
class="inject-input"
value="${row.InjectValue || ''}"
placeholder="输入注入值"
data-interface="${row.InterfaceName}"
data-struct="${row.ModelStructName}"
${row.isInjecting ? 'disabled' : ''}>
</td>
<td>
<input type="number"
class="frequency-input"
value="${row.InjectFrequency || 100}"
min="10"
max="10000"
step="10"
placeholder="输入频率"
data-interface="${row.InterfaceName}"
data-struct="${row.ModelStructName}"
${row.isInjecting ? 'disabled' : ''}>
</td>
<td>
<div class="action-buttons">
<button class="action-button plot" data-interface="${row.InterfaceName}" data-struct="${row.ModelStructName}">绘图</button>
<button class="action-button inject-once" data-interface="${row.InterfaceName}" data-struct="${row.ModelStructName}" ${row.isInjecting ? 'disabled' : ''}>注入一次</button>
<button class="action-button inject-continuous ${row.isInjecting ? 'active' : ''}" data-interface="${row.InterfaceName}" data-struct="${row.ModelStructName}">${row.isInjecting ? '停止注入' : '连续注入'}</button>
<button class="action-button delete" data-interface="${row.InterfaceName}" data-struct="${row.ModelStructName}" ${row.isInjecting ? 'disabled' : ''}>删除</button>
</div>
</td>
</tr>
`;
}).join('')}
</tbody>
</table>
</div>
</div>
</div>
`;
const injectInputs = this.shadowRoot.querySelectorAll('.inject-input');
// 搜索框事件
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 });
};
});
// 添加列宽调整功能
const table = this.shadowRoot.querySelector('.data-table');
const resizers = table.querySelectorAll('.resizer');
resizers.forEach(resizer => {
let startX, startWidth;
resizer.addEventListener('mousedown', (e) => {
e.preventDefault();
const th = resizer.parentElement;
startX = e.pageX;
startWidth = th.offsetWidth;
const mouseMoveHandler = (e) => {
const width = startWidth + (e.pageX - startX);
if (width > 50) {
th.style.width = width + 'px';
}
};
const mouseUpHandler = () => {
document.removeEventListener('mousemove', mouseMoveHandler);
document.removeEventListener('mouseup', mouseUpHandler);
document.body.style.cursor = 'default';
resizer.classList.remove('active');
};
document.addEventListener('mousemove', mouseMoveHandler);
document.addEventListener('mouseup', mouseUpHandler);
document.body.style.cursor = 'col-resize';
resizer.classList.add('active');
});
});
// 添加注入值输入框的事件处理
injectInputs.forEach(input => {
// 添加输入验证
input.addEventListener('input', (e) => {
const value = e.target.value;
const interfaceName = e.target.dataset.interface;
const modelStructName = e.target.dataset.struct;
// 检查接口类型
const isArray = this.isInterfaceArray(interfaceName, modelStructName);
// 只允许数字、小数点、负号和逗号
const validValue = value.replace(/[^0-9.,-]/g, '');
if (value !== validValue) {
e.target.value = validValue;
}
// 验证格式
const isValid = this.validateInjectValue(validValue, isArray, interfaceName, modelStructName);
e.target.classList.toggle('invalid', !isValid);
// 实时更新表格数据
const row = this.tableData.find(r =>
r.InterfaceName === interfaceName &&
r.ModelStructName === modelStructName
);
if (row) {
row.InjectValue = validValue;
}
});
input.addEventListener('change', (e) => {
const interfaceName = e.target.dataset.interface;
const modelStructName = e.target.dataset.struct;
const value = e.target.value;
// 检查接口类型
const isArray = this.isInterfaceArray(interfaceName, modelStructName);
// 验证格式
if (!this.validateInjectValue(value, isArray, interfaceName, modelStructName)) {
e.target.value = '';
// 清空表格数据中的注入值
const row = this.tableData.find(r =>
r.InterfaceName === interfaceName &&
r.ModelStructName === modelStructName
);
if (row) {
row.InjectValue = '';
}
return;
}
// 更新表格数据
const row = this.tableData.find(r =>
r.InterfaceName === interfaceName &&
r.ModelStructName === modelStructName
);
if (row) {
row.InjectValue = value;
}
});
});
// 添加频率输入框的事件处理
const frequencyInputs = this.shadowRoot.querySelectorAll('.frequency-input');
frequencyInputs.forEach(input => {
input.addEventListener('change', (e) => {
const interfaceName = e.target.dataset.interface;
const modelStructName = e.target.dataset.struct;
const value = parseInt(e.target.value);
// 验证频率范围
if (value < 0 || value > 1000) {
e.target.value = 100; // 默认值
return;
}
// 更新表格数据
const row = this.tableData.find(r =>
r.InterfaceName === interfaceName &&
r.ModelStructName === modelStructName
);
if (row) {
row.InjectFrequency = value;
}
});
});
// 添加注入一次按钮的事件委托
table.addEventListener('click', async (e) => {
const injectButton = e.target.closest('.action-button.inject-once');
if (injectButton) {
// 检查监控状态
const statusIndicator = this.shadowRoot.getElementById('statusIndicator');
if (!statusIndicator || !statusIndicator.classList.contains('active')) {
return;
}
const interfaceName = injectButton.dataset.interface;
const modelStructName = injectButton.dataset.struct;
// 获取对应的注入值
const row = this.tableData.find(r =>
r.InterfaceName === interfaceName &&
r.ModelStructName === modelStructName
);
if (!row) {
console.error('未找到对应的行数据');
return;
}
if (!row.InjectValue || row.InjectValue.trim() === '') {
alert('请先输入注入值');
return;
}
// 检查接口类型和注入值格式
const isArray = this.isInterfaceArray(interfaceName, modelStructName);
if (!this.validateInjectValue(row.InjectValue, isArray, interfaceName, modelStructName)) {
alert(isArray ? '请输入正确格式的数组数据(用逗号分隔的数字)' : '请输入单个数字');
return;
}
try {
// 构造接口数据
const interfaceData = {
[interfaceName]: row.InjectValue
};
// 调用注入接口
const response = await fetch('/api/data-monitor/inject', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
structName: modelStructName,
interfaceNameAndData: JSON.stringify(interfaceData)
})
});
const result = await response.json();
if (!result.success) {
throw new Error(result.message);
}
} catch (error) {
console.error('注入失败:', error);
alert(`注入失败: ${error.message}`);
}
}
});
// 添加删除按钮的事件委托
table.addEventListener('click', (e) => {
const deleteButton = e.target.closest('.action-button.delete');
if (deleteButton) {
const interfaceName = deleteButton.dataset.interface;
const modelStructName = deleteButton.dataset.struct;
this.handleDelete(interfaceName, modelStructName);
}
});
// 添加绘图按钮的事件委托
table.addEventListener('click', (e) => {
const plotButton = e.target.closest('.action-button.plot');
if (plotButton) {
const interfaceName = plotButton.dataset.interface;
const modelStructName = plotButton.dataset.struct;
this.handlePlot(interfaceName, modelStructName);
}
});
// 添加连续注入按钮的事件委托
table.addEventListener('click', async (e) => {
const continuousInjectButton = e.target.closest('.action-button.inject-continuous');
if (continuousInjectButton) {
// 检查监控状态
const statusIndicator = this.shadowRoot.getElementById('statusIndicator');
if (!statusIndicator || !statusIndicator.classList.contains('active')) {
return;
}
const interfaceName = continuousInjectButton.getAttribute('data-interface');
const modelStructName = continuousInjectButton.getAttribute('data-struct');
if (!interfaceName || !modelStructName) {
console.error('按钮缺少必要的数据属性');
return;
}
const row = this.tableData.find(r =>
r.InterfaceName === interfaceName &&
r.ModelStructName === modelStructName
);
if (!row) {
console.error('未找到对应的行数据:', { interfaceName, modelStructName });
return;
}
// 如果正在连续注入,则停止注入
if (continuousInjectButton.classList.contains('active')) {
try {
// 调用停止注入接口
const response = await fetch('/api/data-monitor/stop-continuous', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
structName: modelStructName,
interfaceName: interfaceName
})
});
const result = await response.json();
if (!result.success) {
throw new Error(result.message);
}
// 更新按钮和输入框状态
continuousInjectButton.textContent = '连续注入';
continuousInjectButton.classList.remove('active');
const tr = continuousInjectButton.closest('tr');
const input = tr.querySelector('.inject-input');
const injectButton = tr.querySelector('.action-button.inject-once');
const frequencyInput = tr.querySelector('.frequency-input');
const deleteButton = tr.querySelector('.action-button.delete');
input.disabled = false;
injectButton.disabled = false;
frequencyInput.disabled = false;
deleteButton.disabled = false;
// 更新表格数据状态
row.isInjecting = false;
} catch (error) {
console.error('停止注入失败:', error);
alert(`停止注入失败: ${error.message}`);
}
return;
}
if (!row.InjectValue || row.InjectValue.trim() === '') {
alert('请先输入注入值');
return;
}
// 检查接口类型和注入值格式
const isArray = this.isInterfaceArray(interfaceName, modelStructName);
if (!this.validateInjectValue(row.InjectValue, isArray, interfaceName, modelStructName)) {
alert(isArray ? '请输入正确格式的数组数据(用逗号分隔的数字)' : '请输入单个数字');
return;
}
try {
// 构造接口数据
const interfaceData = {
[interfaceName]: row.InjectValue
};
// 调用连续注入接口
const response = await fetch('/api/data-monitor/start-continuous', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
structName: modelStructName,
interfaceNameAndData: JSON.stringify(interfaceData),
frequency: row.InjectFrequency || 100
})
});
const result = await response.json();
if (!result.success) {
throw new Error(result.message);
}
// 更新按钮和输入框状态
continuousInjectButton.textContent = '停止注入';
continuousInjectButton.classList.add('active');
const tr = continuousInjectButton.closest('tr');
const input = tr.querySelector('.inject-input');
const injectButton = tr.querySelector('.action-button.inject-once');
const frequencyInput = tr.querySelector('.frequency-input');
const deleteButton = tr.querySelector('.action-button.delete');
input.disabled = true;
injectButton.disabled = true;
frequencyInput.disabled = true;
deleteButton.disabled = true;
// 更新表格数据状态
row.isInjecting = true;
} catch (error) {
console.error('连续注入失败:', error);
alert(`连续注入失败: ${error.message}`);
}
}
});
}
/**
* @description 验证注入值的格式
* @param {string} value - 要验证的值
* @param {boolean} isArray - 是否为数组类型
* @param {string} interfaceName - 接口名称
* @param {string} modelStructName - 结构体名称
* @returns {boolean} 是否有效
*/
validateInjectValue(value, isArray = false, interfaceName = '', modelStructName = '') {
if (!value) return true; // 空值视为有效
// 检查是否包含非法字符
if (/[^0-9.,-]/.test(value)) return false;
// 检查逗号分隔的数字
const numbers = value.split(',');
// 如果不是数组类型,不应该有逗号
if (!isArray && numbers.length > 1) return false;
// 验证每个数字的格式
for (const num of numbers) {
if (num && !/^-?\d*\.?\d*$/.test(num)) return false;
}
// 如果是数组类型,验证数组大小
if (isArray && interfaceName && modelStructName) {
const interfaceInfo = this.interfaces.find(item =>
item.InterfaceName === interfaceName &&
item.ModelStructName === modelStructName
);
if (interfaceInfo) {
const size1 = interfaceInfo.InterfaceArraySize_1 || 0;
const size2 = interfaceInfo.InterfaceArraySize_2 || 0;
const expectedSize = size2 <= 1 ? size1 : size1 * size2;
if (numbers.length !== expectedSize) {
return false;
}
}
}
return true;
}
/**
* @description 检查接口是否为数组类型
* @param {string} interfaceName - 接口名称
* @param {string} modelStructName - 结构体名称
* @returns {boolean} 是否为数组类型
*/
isInterfaceArray(interfaceName, modelStructName) {
// 从接口数据中查找对应的接口信息
const interfaceInfo = this.interfaces.find(item =>
item.InterfaceName === interfaceName &&
item.ModelStructName === modelStructName
);
// 如果找到接口信息,检查是否为数组类型
return interfaceInfo ? interfaceInfo.InterfaceIsArray : false;
}
/**
* @description 处理删除按钮点击事件
* @param {string} interfaceName - 接口名称
* @param {string} modelStructName - 结构体名称
*/
async handleDelete(interfaceName, modelStructName) {
try {
// 检查是否还有其他相同结构体的接口
const sameStructInterfaces = this.tableData.filter(row =>
row.ModelStructName === modelStructName &&
!(row.InterfaceName === interfaceName && row.ModelStructName === modelStructName)
);
// 如果当前正在监控中,并且这是该结构体的最后一个接口,停止对该结构体的监控
const statusIndicator = this.shadowRoot.getElementById('statusIndicator');
const isMonitoring = statusIndicator && statusIndicator.classList.contains('active');
if (isMonitoring && sameStructInterfaces.length === 0) {
await fetch('/api/data-monitor/stop', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ structName: modelStructName })
});
}
// 从表格数据中移除
this.tableData = this.tableData.filter(row =>
!(row.InterfaceName === interfaceName && row.ModelStructName === modelStructName)
);
// 找到并移除对应的表格行
const rows = this.shadowRoot.querySelectorAll('.data-table tbody tr');
rows.forEach(row => {
if (row.cells[0].textContent === interfaceName && row.cells[1].textContent === modelStructName) {
row.remove();
}
});
} catch (error) {
console.error('删除接口时发生错误:', error);
}
}
disconnectedCallback() {
this.isActive = false;
// 清理所有图表窗口
this.chartWindows.forEach((window, windowId) => {
// 触发关闭事件,确保所有清理工作都完成
window.dispatchEvent(new CustomEvent('close'));
window.remove();
});
this.chartWindows.clear();
// 组件销毁时清理定时器
this.stopDataUpdateTimer();
}
}
customElements.define('data-monitor', DataMonitor);