/**
* @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.csvState = { // CSV 相关状态
isInjecting: false,
fileName: '',
structNames: [],
structData: {}, // 用于存储结构体数据
filePath: '' // 添加文件路径
};
this.monitorStatus = 0; // 监控状态:0-未监控,1-监控中,2-错误
// 绑定方法
this.handleSearch = this.handleSearch.bind(this);
this.handleTreeItemDblClick = this.handleTreeItemDblClick.bind(this);
this.handlePlot = this.handlePlot.bind(this);
this.handleDelete = this.handleDelete.bind(this);
this.handleCsvFileSelect = this.handleCsvFileSelect.bind(this);
this.validateCsvFile = this.validateCsvFile.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;
});
// 初始状态设置为未监控
this.monitorStatus = 0;
this.updateMonitorStatus();
} catch (error) {
console.error('初始化组件失败:', error);
this.monitorStatus = 2;
this.updateMonitorStatus();
}
}
updateMonitorStatus() {
const statusIndicator = this.shadowRoot.getElementById('statusIndicator');
const statusText = this.shadowRoot.getElementById('statusText');
if (statusIndicator) {
statusIndicator.classList.remove('active', 'inactive', 'error');
switch (this.monitorStatus) {
case 1:
statusIndicator.classList.add('active');
break;
case 2:
statusIndicator.classList.add('error');
break;
default:
statusIndicator.classList.add('inactive');
}
}
if (statusText) {
switch (this.monitorStatus) {
case 1:
statusText.textContent = '监控中';
break;
case 2:
statusText.textContent = '监控错误';
break;
default:
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);
// 只更新树型控件部分
const treeView = this.shadowRoot.querySelector('.tree-view');
if (treeView) {
// 按ModelStructName分组
const groupedInterfaces = this.filteredInterfaces.reduce((groups, item) => {
const group = groups[item.ModelStructName] || [];
group.push(item);
groups[item.ModelStructName] = group;
return groups;
}, {});
// 更新树型控件内容
treeView.innerHTML = Object.entries(groupedInterfaces).map(([groupName, items]) => `
${items.map(item => `
${item.InterfaceName}
`).join('')}
`).join('');
// 重新绑定树节点双击事件
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 });
};
});
}
// 在下一个事件循环中恢复焦点和光标位置
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 = `
${item.InterfaceName} |
${item.ModelStructName} |
- |
|
|
|
`;
tbody.appendChild(tr);
// 如果CSV正在注入,禁用除绘图外的按钮
if (this.csvState.isInjecting) {
const actionButtons = tr.querySelectorAll('.action-button:not(.plot)');
actionButtons.forEach(button => {
button.disabled = true;
});
}
}
}
/**
* @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} 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 {
// 检查CSV注入状态
if (this.csvState.isInjecting) {
const csvStatusResponse = await fetch('/api/data-monitor/csv-inject-status');
if (!csvStatusResponse.ok) {
throw new Error(`获取CSV注入状态失败: ${csvStatusResponse.status} ${csvStatusResponse.statusText}`);
}
const csvStatusData = await csvStatusResponse.json();
// 如果状态为0,触发停止注入
if (csvStatusData.data === 0) {
// 模拟点击停止CSV注入按钮
const csvInjectButton = this.shadowRoot.getElementById('csvInjectButton');
if (csvInjectButton) {
csvInjectButton.click();
}
}
}
// 检查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) {
this.monitorStatus = 0;
this.updateMonitorStatus();
// 更新表格中所有数据的监控状态
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已初始化,更新状态显示
this.monitorStatus = 1;
this.updateMonitorStatus();
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();
this.monitorStatus = 2;
this.updateMonitorStatus();
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) {
// 检查监控状态
if (this.monitorStatus !== 1) {
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() {
// 按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 = `
${Object.entries(groupedInterfaces).map(([groupName, items]) => `
${items.map(item => `
${item.InterfaceName}
`).join('')}
`).join('')}
接口名 |
结构体名 |
数据 |
注入值 |
注入频率(Hz) |
操作 |
${this.tableData.map(row => {
return `
${row.InterfaceName} |
${row.ModelStructName} |
${this.formatMonitorData(row.monitorData)} |
|
|
|
`;
}).join('')}
`;
// 搜索框事件
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');
});
});
// 使用事件委托处理输入框事件
table.addEventListener('input', (e) => {
const input = e.target.closest('.inject-input');
if (!input) return;
const value = input.value;
const interfaceName = input.dataset.interface;
const modelStructName = input.dataset.struct;
// 检查接口类型
const isArray = this.isInterfaceArray(interfaceName, modelStructName);
// 只允许数字、小数点、负号和逗号
const validValue = value.replace(/[^0-9.,-]/g, '');
if (value !== validValue) {
input.value = validValue;
}
// 验证格式
const isValid = this.validateInjectValue(validValue, isArray, interfaceName, modelStructName);
input.classList.toggle('invalid', !isValid);
// 实时更新表格数据
const row = this.tableData.find(r =>
r.InterfaceName === interfaceName &&
r.ModelStructName === modelStructName
);
if (row) {
row.InjectValue = validValue;
}
});
table.addEventListener('change', (e) => {
const input = e.target.closest('.inject-input');
if (!input) return;
const interfaceName = input.dataset.interface;
const modelStructName = input.dataset.struct;
const value = input.value;
// 检查接口类型
const isArray = this.isInterfaceArray(interfaceName, modelStructName);
// 验证格式
if (!this.validateInjectValue(value, isArray, interfaceName, modelStructName)) {
input.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) {
// 检查监控状态
if (this.monitorStatus !== 1) {
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) {
// 检查监控状态
if (this.monitorStatus !== 1) {
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}`);
}
}
});
// 添加CSV文件注入相关的事件监听
const csvUploadButton = this.shadowRoot.getElementById('csvUploadButton');
const csvInjectButton = this.shadowRoot.getElementById('csvInjectButton');
const fileInput = this.shadowRoot.getElementById('csvFileInput');
if (csvUploadButton && fileInput) {
csvUploadButton.addEventListener('click', () => {
fileInput.click();
});
}
if (fileInput) {
fileInput.addEventListener('change', this.handleCsvFileSelect);
}
if (csvInjectButton) {
csvInjectButton.addEventListener('click', async () => {
// 检查监控状态
if (this.monitorStatus !== 1) {
return;
}
// 如果按钮文本是"CSV停止注入",则停止注入
if (csvInjectButton.textContent.trim() === 'CSV停止注入') {
try {
const response = await fetch('/api/data-monitor/stop-csv-inject', {
method: 'POST'
});
const result = await response.json();
if (!result.success) {
throw new Error(result.message || '停止CSV注入失败');
}
// 恢复按钮状态
csvUploadButton.disabled = false;
csvInjectButton.textContent = 'CSV数据注入';
csvInjectButton.style.display = 'flex';
// 恢复表格中除绘图按钮外的所有按钮的状态
const actionButtons = this.shadowRoot.querySelectorAll('.action-button:not(.plot)');
actionButtons.forEach(button => {
button.disabled = false;
});
// 恢复文件名显示
const csvFileName = this.shadowRoot.getElementById('csvFileName');
if (csvFileName) {
csvFileName.textContent = this.csvState.fileName;
}
// 更新CSV状态
this.csvState.isInjecting = false;
} catch (error) {
console.error('停止CSV注入失败:', error);
alert(`停止CSV注入失败: ${error.message}`);
}
return;
}
// 弹出确认对话框
if (!confirm('此操作将停止所有持续注入,是否继续?')) {
return;
}
try {
// 获取文件名元素
const csvFileName = this.shadowRoot.getElementById('csvFileName');
if (!csvFileName) {
throw new Error('找不到文件名元素');
}
// 停止所有持续注入
const continuousInjectButtons = this.shadowRoot.querySelectorAll('.action-button.inject-continuous.active');
for (const button of continuousInjectButtons) {
const interfaceName = button.getAttribute('data-interface');
const modelStructName = button.getAttribute('data-struct');
if (interfaceName && modelStructName) {
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 || '停止持续注入失败');
}
// 更新按钮状态
button.textContent = '连续注入';
button.classList.remove('active');
const tr = button.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;
// 更新表格数据状态
const row = this.tableData.find(r =>
r.InterfaceName === interfaceName &&
r.ModelStructName === modelStructName
);
if (row) {
row.isInjecting = false;
}
}
}
// 调用CSV注入接口
const response = await fetch('/api/data-monitor/inject-csv', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
structName: JSON.stringify(this.csvState.structData),
csvFilePath: this.csvState.filePath
})
});
const result = await response.json();
if (!result.success) {
throw new Error(result.message || '启动CSV注入失败');
}
// 禁用上传按钮
csvUploadButton.disabled = true;
// 更新注入按钮文本
csvInjectButton.textContent = 'CSV停止注入';
// 禁用表格中的除绘图按钮外的所有操作按钮
const actionButtons = this.shadowRoot.querySelectorAll('.action-button:not(.plot)');
actionButtons.forEach(button => {
button.disabled = true;
});
// 更新文件名显示
if (csvFileName) {
csvFileName.textContent = this.csvState.fileName + ' 正在注入中...';
}
// 更新CSV状态
this.csvState.isInjecting = true;
} catch (error) {
console.error('CSV注入失败:', error);
alert(`CSV注入失败: ${error.message}`);
}
});
}
// 恢复CSV状态
if (this.csvState.fileName) {
const csvFileName = this.shadowRoot.getElementById('csvFileName');
if (csvFileName) {
csvFileName.textContent = this.csvState.fileName;
if (this.csvState.isInjecting) {
csvFileName.textContent += ' 正在注入中...';
}
}
const csvInjectButton = this.shadowRoot.getElementById('csvInjectButton');
if (csvInjectButton) {
csvInjectButton.style.display = 'flex';
if (this.csvState.isInjecting) {
csvInjectButton.textContent = 'CSV停止注入';
csvUploadButton.disabled = true;
// 禁用表格中除绘图外的所有按钮
const actionButtons = this.shadowRoot.querySelectorAll('.action-button:not(.plot)');
actionButtons.forEach(button => {
button.disabled = true;
});
}
}
}
}
/**
* @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)
);
// 如果当前正在监控中,并且这是该结构体的最后一个接口,停止对该结构体的监控
if (this.monitorStatus === 1 && 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);
}
}
/**
* @description 处理CSV文件选择事件
* @param {Event} event - 文件选择事件
*/
async handleCsvFileSelect(event) {
const file = event.target.files[0];
if (!file) return;
if (!this.validateCsvFile(file)) {
return;
}
try {
// 创建 FormData 对象
const formData = new FormData();
formData.append('file', file);
// 上传文件
const response = await fetch('/api/filesystem/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.message || '文件上传失败');
}
if (!result.success) {
throw new Error(result.message || '文件上传失败');
}
// 验证CSV文件头部
const validateResponse = await fetch(`/api/filesystem/validate-csv-headers?filename=${encodeURIComponent(result.file.name)}`);
if (!validateResponse.ok) {
throw new Error('验证CSV文件头部失败');
}
const validateResult = await validateResponse.json();
if (!validateResult.success) {
throw new Error(validateResult.message || '验证CSV文件头部失败');
}
// 检查每个头部是否在接口表中
const missingInterfaces = [];
const invalidInterfaces = [];
const structData = {}; // 用于存储结构体数据
// 检查第一个接口(时间)是否在接口表中
const firstHeader = validateResult.headers[0];
const firstHeaderExists = this.interfaces.some(interfaceItem =>
interfaceItem.InterfaceName === firstHeader
);
if (firstHeaderExists) {
invalidInterfaces.push(`第一个接口 "${firstHeader}" 不应该在接口表中`);
}
// 检查其他接口是否在接口表中
for (let i = 1; i < validateResult.headers.length; i++) {
const header = validateResult.headers[i];
// 提取接口名称和数组索引
const match = header.match(/^(.+?)\((\d+)(?:_(\d+))?\)$/);
if (!match) continue;
const baseInterfaceName = match[1];
const index1 = match[2] ? parseInt(match[2], 10) - 1 : null;
const index2 = match[3] ? parseInt(match[3], 10) - 1 : null;
const interfaceInfo = this.interfaces.find(interfaceItem =>
interfaceItem.InterfaceName === baseInterfaceName
);
if (!interfaceInfo) {
missingInterfaces.push(header);
} else {
// 构造结构体数据
if (!structData[interfaceInfo.ModelStructName]) {
structData[interfaceInfo.ModelStructName] = {};
}
if (!structData[interfaceInfo.ModelStructName][baseInterfaceName]) {
const size1 = interfaceInfo.InterfaceArraySize_1 || 0;
const size2 = interfaceInfo.InterfaceArraySize_2 || 0;
structData[interfaceInfo.ModelStructName][baseInterfaceName] = [size1, size2];
}
// 检查数组索引是否越界
if (index1 !== null) {
if (index2 !== null) {
// 二维数组
if (index1 >= interfaceInfo.InterfaceArraySize_1 || index2 >= interfaceInfo.InterfaceArraySize_2 || index1 < 0 || index2 < 0) {
console.warn(`接口 ${baseInterfaceName} 的数组索引 [${index1}][${index2}] 超出范围`);
continue;
}
} else {
// 一维数组
if (index1 >= interfaceInfo.InterfaceArraySize_1 || index1 < 0) {
console.warn(`接口 ${baseInterfaceName} 的数组索引 [${index1}] 超出范围`);
continue;
}
}
}
}
}
// 合并错误信息
const errorMessages = [];
if (invalidInterfaces.length > 0) {
errorMessages.push(invalidInterfaces.join('\n'));
}
if (missingInterfaces.length > 0) {
errorMessages.push(`以下接口在系统中不存在:\n${missingInterfaces.join('\n')}`);
}
if (errorMessages.length > 0) {
// 删除已上传的文件
try {
const deleteResponse = await fetch(`/api/filesystem/upload/${encodeURIComponent(result.file.name)}`, {
method: 'DELETE'
});
if (!deleteResponse.ok) {
console.error('删除验证失败的文件时出错');
}
} catch (deleteError) {
console.error('删除验证失败的文件时出错:', deleteError);
}
throw new Error(errorMessages.join('\n\n'));
}
// 更新文件名显示
const csvFileName = this.shadowRoot.getElementById('csvFileName');
if (csvFileName) {
csvFileName.textContent = result.file.name;
// 更新CSV状态
this.csvState.fileName = result.file.name;
this.csvState.structData = structData; // 保存结构体数据
this.csvState.filePath = result.file.path;
}
// 显示数据注入按钮
const csvInjectButton = this.shadowRoot.getElementById('csvInjectButton');
if (csvInjectButton) {
csvInjectButton.style.display = 'flex';
}
} catch (error) {
console.error('CSV文件处理失败:', error);
alert(`CSV文件处理失败: ${error.message}`);
}
}
/**
* @description 验证CSV文件
* @param {File} file - CSV文件对象
* @returns {boolean} 是否验证通过
*/
validateCsvFile(file) {
// 检查文件类型
if (!file.name.toLowerCase().endsWith('.csv')) {
alert('请选择CSV文件');
return false;
}
// 检查文件大小(限制为10MB)
const maxSize = 10 * 1024 * 1024; // 10MB
if (file.size > maxSize) {
alert('文件大小不能超过10MB');
return false;
}
return true;
}
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);