XNSim/XNSimHtml/components/data-monitor.js

2221 lines
85 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.csvState = { // CSV 相关状态
isInjecting: false,
fileName: '',
structNames: [],
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]) => `
<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('');
// 重新绑定树节点双击事件
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 = `
<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);
// 如果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<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();
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 = `
<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;
min-width: 300px;
}
.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;
min-height: 200px;
width: 100%;
}
.tree-group {
margin-bottom: 8px;
width: 100%;
}
.group-header {
font-weight: bold;
padding: 8px;
background-color: #f5f5f5;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
width: 100%;
box-sizing: border-box;
}
.group-content {
margin-left: 20px;
display: block;
width: calc(100% - 20px);
}
.group-content.collapsed {
display: none;
}
.interface-item {
padding: 6px 8px;
cursor: pointer;
border-radius: 4px;
margin: 2px 0;
user-select: none;
width: 100%;
box-sizing: border-box;
}
.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;
}
.csv-inject-button {
padding: 6px 12px;
background: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.3s;
}
.csv-inject-button:hover {
background: #40a9ff;
}
.csv-inject-button:disabled {
background: #d9d9d9;
cursor: not-allowed;
}
.csv-file-name {
font-size: 14px;
color: #666;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: none;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-overlay.active {
display: flex;
}
.modal-content {
background: white;
border-radius: 8px;
padding: 24px;
width: 400px;
max-width: 90%;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.modal-title {
font-size: 18px;
font-weight: 500;
color: #262626;
margin: 0;
}
.modal-close {
background: none;
border: none;
font-size: 20px;
color: #999;
cursor: pointer;
padding: 4px;
line-height: 1;
}
.modal-close:hover {
color: #666;
}
.modal-body {
margin-bottom: 20px;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #262626;
font-size: 14px;
}
.form-group input {
width: 100%;
padding: 8px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
.form-group input:focus {
border-color: #1890ff;
outline: none;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.modal-button {
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
}
.modal-button.primary {
background: #1890ff;
color: white;
border: none;
}
.modal-button.primary:hover {
background: #40a9ff;
}
.modal-button.secondary {
background: white;
color: #262626;
border: 1px solid #d9d9d9;
}
.modal-button.secondary:hover {
border-color: #1890ff;
color: #1890ff;
}
.file-input-wrapper {
position: relative;
display: inline-block;
width: 100%;
}
.file-input-wrapper input[type="file"] {
position: absolute;
left: 0;
top: 0;
opacity: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
.file-input-trigger {
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
border: 1px dashed #d9d9d9;
border-radius: 4px;
color: #666;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
}
.file-input-trigger:hover {
border-color: #1890ff;
color: #1890ff;
}
.file-name {
margin-top: 8px;
font-size: 14px;
color: #666;
word-break: break-all;
}
</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 class="toolbar-right">
<input type="file" id="csvFileInput" accept=".csv" style="display: none;" />
<span id="csvFileName" class="csv-file-name"></span>
<button class="csv-inject-button" id="csvUploadButton">
<img src="assets/icons/png/upload.png" alt="上传" style="width: 16px; height: 16px;">
上传CSV文件
</button>
<button class="csv-inject-button" id="csvInjectButton" style="display: none;">
<img src="assets/icons/png/inject.png" alt="注入" style="width: 16px; height: 16px;">
CSV数据注入
</button>
</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 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.structNames),
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 structNames = []; // 用于按顺序收集结构体名称
// 检查第一个接口(时间)是否在接口表中
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 interfaceInfo = this.interfaces.find(interfaceItem =>
interfaceItem.InterfaceName === header
);
if (!interfaceInfo) {
missingInterfaces.push(header);
} else {
// 按顺序收集结构体名称
structNames.push(interfaceInfo.ModelStructName);
}
}
// 合并错误信息
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.structNames = structNames;
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);