2025-06-03 16:55:53 +08:00
|
|
|
|
/**
|
|
|
|
|
* @class DataMonitor
|
|
|
|
|
* @extends HTMLElement
|
|
|
|
|
* @description 数据监控组件的基础类
|
|
|
|
|
*/
|
2025-04-28 12:25:20 +08:00
|
|
|
|
class DataMonitor extends HTMLElement {
|
|
|
|
|
constructor() {
|
|
|
|
|
super();
|
|
|
|
|
this.attachShadow({ mode: 'open' });
|
2025-06-03 16:55:53 +08:00
|
|
|
|
this.interfaces = []; // 存储接口数据
|
|
|
|
|
this.filteredInterfaces = []; // 存储过滤后的接口数据
|
|
|
|
|
this.searchText = ''; // 搜索文本
|
|
|
|
|
this.searchTimeout = null; // 用于防抖的定时器
|
|
|
|
|
this.cursorPosition = 0; // 存储光标位置
|
|
|
|
|
this.tableData = []; // 表格数据,存储已添加的接口
|
2025-06-06 11:02:12 +08:00
|
|
|
|
this.monitorId = `data_monitor_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; // 添加监控器ID
|
|
|
|
|
this.dataUpdateTimer = null; // 数据更新定时器
|
2025-06-09 13:46:52 +08:00
|
|
|
|
this.isActive = false; // 组件是否激活
|
2025-06-10 15:21:07 +08:00
|
|
|
|
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);
|
2025-06-11 16:39:04 +08:00
|
|
|
|
this.handleCsvInject = this.handleCsvInject.bind(this);
|
|
|
|
|
this.handleModalClose = this.handleModalClose.bind(this);
|
|
|
|
|
this.handleModalCancel = this.handleModalCancel.bind(this);
|
|
|
|
|
this.handleModalConfirm = this.handleModalConfirm.bind(this);
|
|
|
|
|
this.handleFileInput = this.handleFileInput.bind(this);
|
|
|
|
|
this.validateCsvFile = this.validateCsvFile.bind(this);
|
2025-06-10 15:21:07 +08:00
|
|
|
|
|
|
|
|
|
// 确保 FloatingChartWindow 组件已注册
|
|
|
|
|
if (!customElements.get('floating-chart-window')) {
|
|
|
|
|
const script = document.createElement('script');
|
|
|
|
|
script.src = './components/FloatingChartWindow.js';
|
|
|
|
|
document.head.appendChild(script);
|
|
|
|
|
}
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
connectedCallback() {
|
2025-06-09 13:46:52 +08:00
|
|
|
|
this.isActive = true; // 设置初始状态为激活
|
2025-06-03 16:55:53 +08:00
|
|
|
|
this.loadInterfaces();
|
2025-06-10 15:21:07 +08:00
|
|
|
|
|
2025-06-09 13:46:52 +08:00
|
|
|
|
// 等待组件完全加载后初始化
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.initializeComponent();
|
|
|
|
|
// 初始化完成后再启动定时器,给服务器一些准备时间
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.startDataUpdateTimer();
|
|
|
|
|
}, 1000); // 延迟1秒启动定时器
|
|
|
|
|
}, 100);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 重新激活组件
|
|
|
|
|
reactivate() {
|
|
|
|
|
if (this.isActive) return; // 如果已经激活,直接返回
|
|
|
|
|
this.isActive = true;
|
|
|
|
|
this.initializeComponent();
|
2025-06-09 14:39:29 +08:00
|
|
|
|
// 重新启动定时器
|
|
|
|
|
this.startDataUpdateTimer();
|
2025-06-11 15:25:25 +08:00
|
|
|
|
|
2025-06-09 13:46:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async initializeComponent() {
|
|
|
|
|
try {
|
|
|
|
|
// 更新所有行的监控状态
|
|
|
|
|
this.tableData.forEach(row => {
|
|
|
|
|
row.isMonitoring = true;
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-09 14:39:29 +08:00
|
|
|
|
// 初始状态设置为未监控
|
2025-06-09 13:46:52 +08:00
|
|
|
|
const statusIndicator = this.shadowRoot.getElementById('statusIndicator');
|
|
|
|
|
const statusText = this.shadowRoot.getElementById('statusText');
|
|
|
|
|
if (statusIndicator) {
|
2025-06-09 14:39:29 +08:00
|
|
|
|
statusIndicator.classList.remove('active', 'error');
|
|
|
|
|
statusIndicator.classList.add('inactive');
|
2025-06-09 13:46:52 +08:00
|
|
|
|
}
|
|
|
|
|
if (statusText) {
|
2025-06-09 14:39:29 +08:00
|
|
|
|
statusText.textContent = '未监控';
|
2025-06-09 13:46:52 +08:00
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('初始化组件失败:', error);
|
|
|
|
|
const statusIndicator = this.shadowRoot.getElementById('statusIndicator');
|
|
|
|
|
const statusText = this.shadowRoot.getElementById('statusText');
|
|
|
|
|
if (statusIndicator) {
|
2025-06-09 14:39:29 +08:00
|
|
|
|
statusIndicator.classList.remove('active', 'inactive');
|
2025-06-09 13:46:52 +08:00
|
|
|
|
statusIndicator.classList.add('error');
|
|
|
|
|
}
|
|
|
|
|
if (statusText) {
|
|
|
|
|
statusText.textContent = '监控错误';
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 16:55:53 +08:00
|
|
|
|
/**
|
|
|
|
|
* @description 从localStorage获取当前选择的配置
|
2025-06-06 11:02:12 +08:00
|
|
|
|
* @returns {Object} 包含plane、configurationId和domainId的对象
|
2025-06-03 16:55:53 +08:00
|
|
|
|
*/
|
|
|
|
|
getCurrentSelection() {
|
|
|
|
|
const selection = localStorage.getItem('xnsim-selection');
|
2025-06-06 11:02:12 +08:00
|
|
|
|
|
|
|
|
|
if (!selection) {
|
|
|
|
|
return { plane: '', configurationId: '', domainId: '' };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const parsedSelection = JSON.parse(selection);
|
|
|
|
|
return {
|
|
|
|
|
plane: parsedSelection.plane || '',
|
|
|
|
|
configurationId: parsedSelection.configurationId || '',
|
|
|
|
|
domainId: parsedSelection.domainId || ''
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return { plane: '', configurationId: '', domainId: '' };
|
|
|
|
|
}
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 16:55:53 +08:00
|
|
|
|
/**
|
|
|
|
|
* @description 加载接口数据
|
|
|
|
|
*/
|
|
|
|
|
async loadInterfaces() {
|
2025-04-28 16:41:21 +08:00
|
|
|
|
try {
|
2025-06-03 16:55:53 +08:00
|
|
|
|
const { configurationId } = this.getCurrentSelection();
|
|
|
|
|
if (!configurationId) {
|
|
|
|
|
console.warn('未找到配置ID');
|
2025-04-28 16:41:21 +08:00
|
|
|
|
return;
|
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
|
|
|
|
|
const response = await fetch(`/api/interface/list?systemName=XNSim&confID=${configurationId}`);
|
2025-04-28 16:41:21 +08:00
|
|
|
|
const data = await response.json();
|
2025-06-03 16:55:53 +08:00
|
|
|
|
this.interfaces = data;
|
|
|
|
|
this.filteredInterfaces = this.filterInterfaces(this.searchText);
|
|
|
|
|
this.render();
|
2025-04-28 16:41:21 +08:00
|
|
|
|
} catch (error) {
|
2025-06-03 16:55:53 +08:00
|
|
|
|
console.error('加载接口数据失败:', error);
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 16:55:53 +08:00
|
|
|
|
/**
|
|
|
|
|
* @description 根据搜索文本过滤接口
|
|
|
|
|
* @param {string} searchText - 搜索文本
|
|
|
|
|
* @returns {Array} 过滤后的接口数据
|
|
|
|
|
*/
|
|
|
|
|
filterInterfaces(searchText) {
|
|
|
|
|
if (!searchText) return this.interfaces;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
|
2025-06-03 16:55:53 +08:00
|
|
|
|
return this.interfaces.filter(item =>
|
|
|
|
|
item.InterfaceName.toLowerCase().includes(searchText.toLowerCase()) ||
|
|
|
|
|
item.ModelStructName.toLowerCase().includes(searchText.toLowerCase())
|
|
|
|
|
);
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 16:55:53 +08:00
|
|
|
|
/**
|
|
|
|
|
* @description 处理搜索输入
|
|
|
|
|
* @param {Event} event - 输入事件
|
|
|
|
|
*/
|
|
|
|
|
handleSearch(event) {
|
|
|
|
|
this.searchText = event.target.value;
|
|
|
|
|
this.cursorPosition = event.target.selectionStart;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
|
2025-06-03 16:55:53 +08:00
|
|
|
|
// 清除之前的定时器
|
|
|
|
|
if (this.searchTimeout) {
|
|
|
|
|
clearTimeout(this.searchTimeout);
|
|
|
|
|
}
|
2025-04-28 16:41:21 +08:00
|
|
|
|
|
2025-06-03 16:55:53 +08:00
|
|
|
|
// 设置新的定时器,300ms后执行搜索
|
|
|
|
|
this.searchTimeout = setTimeout(() => {
|
|
|
|
|
this.filteredInterfaces = this.filterInterfaces(this.searchText);
|
2025-06-11 15:35:46 +08:00
|
|
|
|
|
|
|
|
|
// 只更新树型控件部分
|
|
|
|
|
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 });
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
|
|
|
|
|
// 在下一个事件循环中恢复焦点和光标位置
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
const searchInput = this.shadowRoot.querySelector('.search-input');
|
|
|
|
|
if (searchInput) {
|
|
|
|
|
searchInput.focus();
|
|
|
|
|
searchInput.setSelectionRange(this.cursorPosition, this.cursorPosition);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}, 300);
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @description 处理树节点双击事件,将接口添加到表格
|
|
|
|
|
* @param {Object} item - 接口对象
|
|
|
|
|
*/
|
|
|
|
|
handleTreeItemDblClick(item) {
|
|
|
|
|
// 防止重复添加
|
|
|
|
|
if (!this.tableData.some(row => row.InterfaceName === item.InterfaceName && row.ModelStructName === item.ModelStructName)) {
|
2025-06-10 15:21:07 +08:00
|
|
|
|
// 添加新数据
|
2025-06-03 16:55:53 +08:00
|
|
|
|
this.tableData.push({
|
|
|
|
|
InterfaceName: item.InterfaceName,
|
|
|
|
|
ModelStructName: item.ModelStructName,
|
|
|
|
|
InjectValue: '',
|
2025-06-11 15:25:25 +08:00
|
|
|
|
InjectFrequency: 100,
|
2025-06-10 15:21:07 +08:00
|
|
|
|
monitorData: '',
|
2025-06-11 15:25:25 +08:00
|
|
|
|
isMonitoring: false,
|
|
|
|
|
isInjecting: false
|
2025-06-10 15:21:07 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 创建新行
|
|
|
|
|
const tbody = this.shadowRoot.querySelector('.data-table tbody');
|
|
|
|
|
const tr = document.createElement('tr');
|
2025-06-11 15:25:25 +08:00
|
|
|
|
tr.setAttribute('data-interface', item.InterfaceName);
|
|
|
|
|
tr.setAttribute('data-struct', item.ModelStructName);
|
2025-06-10 15:21:07 +08:00
|
|
|
|
tr.innerHTML = `
|
|
|
|
|
<td title="${item.InterfaceName}">${item.InterfaceName}</td>
|
|
|
|
|
<td title="${item.ModelStructName}">${item.ModelStructName}</td>
|
|
|
|
|
<td class="data-cell" title="-">-</td>
|
2025-06-11 10:59:35 +08:00
|
|
|
|
<td>
|
|
|
|
|
<input type="text"
|
|
|
|
|
class="inject-input"
|
|
|
|
|
value=""
|
|
|
|
|
placeholder="输入注入值"
|
|
|
|
|
data-interface="${item.InterfaceName}"
|
|
|
|
|
data-struct="${item.ModelStructName}">
|
|
|
|
|
</td>
|
2025-06-11 15:25:25 +08:00
|
|
|
|
<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>
|
2025-06-10 15:21:07 +08:00
|
|
|
|
<td>
|
|
|
|
|
<div class="action-buttons">
|
|
|
|
|
<button class="action-button plot" data-interface="${item.InterfaceName}" data-struct="${item.ModelStructName}">绘图</button>
|
2025-06-11 10:59:35 +08:00
|
|
|
|
<button class="action-button inject-once" data-interface="${item.InterfaceName}" data-struct="${item.ModelStructName}">注入一次</button>
|
2025-06-11 15:25:25 +08:00
|
|
|
|
<button class="action-button inject-continuous" data-interface="${item.InterfaceName}" data-struct="${item.ModelStructName}">连续注入</button>
|
2025-06-10 15:21:07 +08:00
|
|
|
|
<button class="action-button delete" data-interface="${item.InterfaceName}" data-struct="${item.ModelStructName}">删除</button>
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
tbody.appendChild(tr);
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 11:02:12 +08:00
|
|
|
|
/**
|
|
|
|
|
* @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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-10 15:21:07 +08:00
|
|
|
|
/**
|
|
|
|
|
* @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;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 11:02:12 +08:00
|
|
|
|
/**
|
|
|
|
|
* @description 启动数据更新定时器
|
|
|
|
|
*/
|
|
|
|
|
startDataUpdateTimer() {
|
|
|
|
|
if (this.dataUpdateTimer) {
|
|
|
|
|
clearInterval(this.dataUpdateTimer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.dataUpdateTimer = setInterval(async () => {
|
|
|
|
|
try {
|
2025-06-09 14:39:29 +08:00
|
|
|
|
// 检查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 = '';
|
|
|
|
|
});
|
2025-06-10 15:21:07 +08:00
|
|
|
|
// 只更新数据单元格
|
|
|
|
|
const dataCells = this.shadowRoot.querySelectorAll('.data-cell');
|
|
|
|
|
dataCells.forEach(cell => {
|
|
|
|
|
cell.textContent = '-';
|
|
|
|
|
cell.title = '-';
|
|
|
|
|
});
|
2025-06-09 14:39:29 +08:00
|
|
|
|
return; // 未初始化时等待下一次循环
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DDS已初始化,更新状态显示
|
|
|
|
|
if (statusIndicator) {
|
|
|
|
|
statusIndicator.classList.remove('inactive', 'error');
|
|
|
|
|
statusIndicator.classList.add('active');
|
|
|
|
|
}
|
|
|
|
|
if (statusText) {
|
|
|
|
|
statusText.textContent = '监控中';
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 11:02:12 +08:00
|
|
|
|
const groupedInterfaces = this.getGroupedInterfaces();
|
2025-06-10 16:09:27 +08:00
|
|
|
|
const allData = {}; // 存储所有结构体的数据
|
2025-06-06 11:02:12 +08:00
|
|
|
|
|
|
|
|
|
// 对每个结构体启动监控并获取数据
|
|
|
|
|
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(`获取监控数据失败: 返回数据为空`);
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-10 16:09:27 +08:00
|
|
|
|
// 合并数据
|
|
|
|
|
Object.assign(allData, responseData.data);
|
2025-06-06 11:02:12 +08:00
|
|
|
|
} catch (structError) {
|
|
|
|
|
console.error(`处理结构体 ${structName} 时出错:`, structError);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-10 16:09:27 +08:00
|
|
|
|
|
|
|
|
|
// 一次性更新所有数据
|
|
|
|
|
if (Object.keys(allData).length > 0) {
|
|
|
|
|
this.updateTableData(allData);
|
|
|
|
|
}
|
2025-06-06 11:02:12 +08:00
|
|
|
|
} 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 = '';
|
|
|
|
|
});
|
2025-06-10 15:21:07 +08:00
|
|
|
|
// 只更新数据单元格
|
|
|
|
|
const dataCells = this.shadowRoot.querySelectorAll('.data-cell');
|
|
|
|
|
dataCells.forEach(cell => {
|
|
|
|
|
cell.textContent = '-';
|
|
|
|
|
cell.title = '-';
|
|
|
|
|
});
|
2025-06-06 11:02:12 +08:00
|
|
|
|
}
|
|
|
|
|
}, 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 16:55:53 +08:00
|
|
|
|
/**
|
2025-06-10 15:21:07 +08:00
|
|
|
|
* @description 获取随机颜色
|
|
|
|
|
* @returns {string} 颜色字符串
|
2025-06-03 16:55:53 +08:00
|
|
|
|
*/
|
2025-06-10 15:21:07 +08:00
|
|
|
|
getRandomColor() {
|
|
|
|
|
const letters = '0123456789ABCDEF';
|
|
|
|
|
let color = '#';
|
|
|
|
|
for (let i = 0; i < 6; i++) {
|
|
|
|
|
color += letters[Math.floor(Math.random() * 16)];
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-10 15:21:07 +08:00
|
|
|
|
return color;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-06-10 15:21:07 +08:00
|
|
|
|
* @description 处理绘图按钮点击事件
|
|
|
|
|
* @param {string} interfaceName - 接口名称
|
|
|
|
|
* @param {string} modelStructName - 结构体名称
|
2025-06-03 16:55:53 +08:00
|
|
|
|
*/
|
2025-06-10 15:21:07 +08:00
|
|
|
|
async handlePlot(interfaceName, modelStructName) {
|
2025-06-10 16:09:27 +08:00
|
|
|
|
// 检查监控状态
|
|
|
|
|
const statusIndicator = this.shadowRoot.getElementById('statusIndicator');
|
|
|
|
|
if (!statusIndicator || !statusIndicator.classList.contains('active')) {
|
|
|
|
|
return; // 如果不在监控状态,直接返回
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-10 15:21:07 +08:00
|
|
|
|
// 检查是否已经存在该接口的图表窗口
|
|
|
|
|
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 未加载...');
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-10 15:21:07 +08:00
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 创建图表数据
|
|
|
|
|
const chartData = {
|
2025-06-03 16:55:53 +08:00
|
|
|
|
labels: [],
|
2025-06-10 16:09:27 +08:00
|
|
|
|
datasets: []
|
2025-06-10 15:21:07 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 创建图表配置
|
|
|
|
|
const chartOptions = {
|
2025-06-03 16:55:53 +08:00
|
|
|
|
responsive: true,
|
|
|
|
|
maintainAspectRatio: false,
|
2025-06-10 15:21:07 +08:00
|
|
|
|
animation: false,
|
|
|
|
|
elements: {
|
|
|
|
|
point: {
|
|
|
|
|
radius: 0
|
|
|
|
|
},
|
|
|
|
|
line: {
|
|
|
|
|
tension: 0
|
|
|
|
|
}
|
|
|
|
|
},
|
2025-06-03 16:55:53 +08:00
|
|
|
|
scales: {
|
2025-06-10 15:21:07 +08:00
|
|
|
|
y: {
|
|
|
|
|
beginAtZero: false,
|
2025-06-10 16:09:27 +08:00
|
|
|
|
display: true,
|
2025-06-10 15:21:07 +08:00
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
},
|
|
|
|
|
plugins: {
|
2025-06-10 15:21:07 +08:00
|
|
|
|
legend: {
|
|
|
|
|
display: true,
|
|
|
|
|
position: 'top'
|
|
|
|
|
},
|
|
|
|
|
tooltip: {
|
|
|
|
|
enabled: true,
|
|
|
|
|
mode: 'index',
|
|
|
|
|
intersect: false,
|
|
|
|
|
callbacks: {
|
|
|
|
|
label: function(context) {
|
|
|
|
|
const value = context.raw;
|
2025-06-10 16:09:27 +08:00
|
|
|
|
// 始终使用普通数字格式,保留一位小数
|
2025-06-10 15:21:07 +08:00
|
|
|
|
return `${context.dataset.label}: ${value.toFixed(1)}`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
}
|
2025-06-10 15:21:07 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 创建浮动窗口组件
|
|
|
|
|
const floatingWindow = document.createElement('floating-chart-window');
|
|
|
|
|
if (!floatingWindow) {
|
|
|
|
|
throw new Error('创建浮动窗口组件失败');
|
2025-06-03 16:55:53 +08:00
|
|
|
|
}
|
2025-04-28 16:41:21 +08:00
|
|
|
|
|
2025-06-10 15:21:07 +08:00
|
|
|
|
// 添加数据点计数器
|
|
|
|
|
floatingWindow.dataPointIndex = 0;
|
|
|
|
|
|
|
|
|
|
// 先添加到页面
|
|
|
|
|
document.body.appendChild(floatingWindow);
|
|
|
|
|
this.chartWindows.set(windowId, floatingWindow);
|
|
|
|
|
|
|
|
|
|
// 再设置属性
|
|
|
|
|
floatingWindow.setAttribute('title', interfaceName);
|
2025-06-10 16:09:27 +08:00
|
|
|
|
floatingWindow.setAttribute('initial-position', JSON.stringify({ x: 400, y: 200 }));
|
2025-06-10 15:21:07 +08:00
|
|
|
|
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 {
|
2025-06-10 16:09:27 +08:00
|
|
|
|
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];
|
2025-06-10 15:21:07 +08:00
|
|
|
|
} else {
|
|
|
|
|
// 尝试将字符串转换为数字
|
|
|
|
|
const numValue = parseFloat(data);
|
2025-06-10 16:09:27 +08:00
|
|
|
|
values = isNaN(numValue) ? [] : [numValue];
|
2025-06-10 15:21:07 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
|
2025-06-10 16:09:27 +08:00
|
|
|
|
// 如果数据有效,更新图表
|
|
|
|
|
if (values.length > 0) {
|
|
|
|
|
floatingWindow.handleDataUpdate(values, interfaceName);
|
2025-06-10 15:21:07 +08:00
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('解析数据失败:', e);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
};
|
2025-04-28 16:41:21 +08:00
|
|
|
|
|
2025-06-10 15:21:07 +08:00
|
|
|
|
// 将更新函数添加到数据更新定时器中
|
|
|
|
|
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('创建图表窗口失败,请查看控制台了解详情');
|
2025-06-03 16:55:53 +08:00
|
|
|
|
}
|
2025-06-10 15:21:07 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
|
2025-06-10 15:21:07 +08:00
|
|
|
|
render() {
|
2025-06-10 16:09:27 +08:00
|
|
|
|
// 获取当前监控状态
|
|
|
|
|
const statusIndicator = this.shadowRoot.getElementById('statusIndicator');
|
|
|
|
|
const isMonitoring = statusIndicator && statusIndicator.classList.contains('active');
|
|
|
|
|
|
2025-06-03 16:55:53 +08:00
|
|
|
|
// 按ModelStructName分组
|
|
|
|
|
const groupedInterfaces = this.filteredInterfaces.reduce((groups, item) => {
|
|
|
|
|
const group = groups[item.ModelStructName] || [];
|
|
|
|
|
group.push(item);
|
|
|
|
|
groups[item.ModelStructName] = group;
|
|
|
|
|
return groups;
|
|
|
|
|
}, {});
|
|
|
|
|
|
2025-04-28 12:25:20 +08:00
|
|
|
|
this.shadowRoot.innerHTML = `
|
|
|
|
|
<style>
|
|
|
|
|
:host {
|
|
|
|
|
display: block;
|
|
|
|
|
height: 100%;
|
|
|
|
|
overflow: auto;
|
|
|
|
|
padding: 16px;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 11:02:12 +08:00
|
|
|
|
.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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-09 14:39:29 +08:00
|
|
|
|
.status-indicator.inactive {
|
|
|
|
|
background: #d9d9d9;
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-28 12:25:20 +08:00
|
|
|
|
.monitor-container {
|
|
|
|
|
background-color: white;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
|
|
|
padding: 16px;
|
2025-06-06 11:02:12 +08:00
|
|
|
|
height: calc(100% - 72px);
|
2025-04-28 12:25:20 +08:00
|
|
|
|
box-sizing: border-box;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
display: flex;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
gap: 16px;
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 16:55:53 +08:00
|
|
|
|
.tree-container {
|
|
|
|
|
width: 300px;
|
|
|
|
|
border-right: 1px solid #e0e0e0;
|
|
|
|
|
padding-right: 16px;
|
2025-04-28 12:25:20 +08:00
|
|
|
|
display: flex;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
flex-direction: column;
|
2025-06-11 15:35:46 +08:00
|
|
|
|
min-width: 300px;
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 16:55:53 +08:00
|
|
|
|
.search-box {
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search-input {
|
|
|
|
|
width: 100%;
|
|
|
|
|
padding: 8px;
|
|
|
|
|
border: 1px solid #d9d9d9;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 16:55:53 +08:00
|
|
|
|
.tree-view {
|
|
|
|
|
flex-grow: 1;
|
|
|
|
|
overflow-y: auto;
|
2025-06-11 15:35:46 +08:00
|
|
|
|
min-height: 200px;
|
|
|
|
|
width: 100%;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tree-group {
|
|
|
|
|
margin-bottom: 8px;
|
2025-06-11 15:35:46 +08:00
|
|
|
|
width: 100%;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.group-header {
|
2025-04-28 16:41:21 +08:00
|
|
|
|
font-weight: bold;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
padding: 8px;
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
2025-06-11 15:35:46 +08:00
|
|
|
|
width: 100%;
|
|
|
|
|
box-sizing: border-box;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.group-content {
|
|
|
|
|
margin-left: 20px;
|
|
|
|
|
display: block;
|
2025-06-11 15:35:46 +08:00
|
|
|
|
width: calc(100% - 20px);
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 16:55:53 +08:00
|
|
|
|
.group-content.collapsed {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.interface-item {
|
|
|
|
|
padding: 6px 8px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
margin: 2px 0;
|
|
|
|
|
user-select: none;
|
2025-06-11 15:35:46 +08:00
|
|
|
|
width: 100%;
|
|
|
|
|
box-sizing: border-box;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.interface-item:hover {
|
2025-04-28 16:41:21 +08:00
|
|
|
|
background-color: #f0f0f0;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 16:55:53 +08:00
|
|
|
|
.content-area {
|
2025-04-28 16:41:21 +08:00
|
|
|
|
flex-grow: 1;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
height: 100%;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
min-width: 0;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
overflow: hidden;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
|
2025-06-10 15:21:07 +08:00
|
|
|
|
.table-container {
|
2025-06-03 16:55:53 +08:00
|
|
|
|
flex: 1;
|
2025-06-10 15:21:07 +08:00
|
|
|
|
overflow: auto;
|
|
|
|
|
margin-top: 16px;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
width: 100%;
|
2025-06-10 15:21:07 +08:00
|
|
|
|
position: relative;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
}
|
2025-06-10 15:21:07 +08:00
|
|
|
|
|
|
|
|
|
.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);
|
2025-06-03 16:55:53 +08:00
|
|
|
|
table-layout: fixed;
|
|
|
|
|
}
|
2025-06-10 15:21:07 +08:00
|
|
|
|
|
|
|
|
|
.data-table th,
|
|
|
|
|
.data-table td {
|
|
|
|
|
padding: 12px 16px;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
text-align: left;
|
2025-06-10 15:21:07 +08:00
|
|
|
|
border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
border-right: 1px solid #f0f0f0;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
position: relative;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-10 15:21:07 +08:00
|
|
|
|
|
|
|
|
|
.data-table th:last-child,
|
|
|
|
|
.data-table td:last-child {
|
|
|
|
|
border-right: none;
|
|
|
|
|
width: 300px !important; /* 固定操作列宽度 */
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-10 15:21:07 +08:00
|
|
|
|
|
|
|
|
|
.data-table th:last-child .resizer {
|
|
|
|
|
display: none; /* 隐藏操作列的调整器 */
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-10 15:21:07 +08:00
|
|
|
|
|
|
|
|
|
.data-table th {
|
|
|
|
|
background: #fafafa;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: #262626;
|
|
|
|
|
user-select: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.data-table tr:hover {
|
|
|
|
|
background: #fafafa;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
}
|
2025-06-10 15:21:07 +08:00
|
|
|
|
|
|
|
|
|
.resizer {
|
2025-06-03 16:55:53 +08:00
|
|
|
|
position: absolute;
|
|
|
|
|
right: 0;
|
2025-06-10 15:21:07 +08:00
|
|
|
|
top: 0;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
width: 5px;
|
|
|
|
|
cursor: col-resize;
|
|
|
|
|
background: transparent;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-10 15:21:07 +08:00
|
|
|
|
|
|
|
|
|
.resizer:hover,
|
|
|
|
|
.resizer.active {
|
2025-06-03 16:55:53 +08:00
|
|
|
|
background: #1890ff;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
|
2025-06-10 15:21:07 +08:00
|
|
|
|
.action-buttons {
|
2025-04-28 16:41:21 +08:00
|
|
|
|
display: flex;
|
2025-06-10 15:21:07 +08:00
|
|
|
|
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;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-10 15:21:07 +08:00
|
|
|
|
|
|
|
|
|
.action-button:hover {
|
|
|
|
|
color: #1890ff;
|
|
|
|
|
border-color: #1890ff;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-10 15:21:07 +08:00
|
|
|
|
|
|
|
|
|
.action-button.delete {
|
|
|
|
|
color: #ff4d4f;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
|
2025-06-10 15:21:07 +08:00
|
|
|
|
.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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-11 10:59:35 +08:00
|
|
|
|
.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);
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-11 15:25:25 +08:00
|
|
|
|
.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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-10 15:21:07 +08:00
|
|
|
|
.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);
|
2025-04-28 16:41:21 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
overflow: hidden;
|
2025-06-10 15:21:07 +08:00
|
|
|
|
left: 100px;
|
|
|
|
|
top: 100px;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-10 15:21:07 +08:00
|
|
|
|
|
|
|
|
|
.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;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-10 15:21:07 +08:00
|
|
|
|
|
|
|
|
|
.window-title {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: #262626;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-10 15:21:07 +08:00
|
|
|
|
|
|
|
|
|
.window-controls {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 8px;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
|
2025-06-10 15:21:07 +08:00
|
|
|
|
.window-control-button {
|
|
|
|
|
width: 24px;
|
|
|
|
|
height: 24px;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
border: none;
|
2025-06-10 15:21:07 +08:00
|
|
|
|
background: transparent;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
cursor: pointer;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
display: flex;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
2025-06-10 15:21:07 +08:00
|
|
|
|
font-size: 16px;
|
|
|
|
|
color: #666;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
transition: all 0.3s;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
}
|
2025-06-10 15:21:07 +08:00
|
|
|
|
|
|
|
|
|
.window-control-button:hover {
|
|
|
|
|
background: rgba(0, 0, 0, 0.05);
|
|
|
|
|
color: #262626;
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
2025-06-10 15:21:07 +08:00
|
|
|
|
|
|
|
|
|
.window-control-button.close-button:hover {
|
2025-06-06 11:02:12 +08:00
|
|
|
|
background: #ff4d4f;
|
2025-06-10 15:21:07 +08:00
|
|
|
|
color: white;
|
2025-06-06 11:02:12 +08:00
|
|
|
|
}
|
2025-06-10 15:21:07 +08:00
|
|
|
|
|
|
|
|
|
.window-content {
|
|
|
|
|
flex: 1;
|
|
|
|
|
padding: 12px;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.window-content canvas {
|
|
|
|
|
width: 100% !important;
|
|
|
|
|
height: 100% !important;
|
2025-06-06 11:02:12 +08:00
|
|
|
|
}
|
2025-06-11 15:25:25 +08:00
|
|
|
|
|
|
|
|
|
.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;
|
|
|
|
|
}
|
2025-06-11 16:39:04 +08:00
|
|
|
|
|
|
|
|
|
.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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.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;
|
|
|
|
|
}
|
2025-04-28 12:25:20 +08:00
|
|
|
|
</style>
|
2025-06-06 11:02:12 +08:00
|
|
|
|
<div class="toolbar">
|
|
|
|
|
<div class="toolbar-left">
|
|
|
|
|
<div class="monitor-status">
|
|
|
|
|
<div class="status-indicator" id="statusIndicator"></div>
|
|
|
|
|
<span id="statusText">未监控</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-06-11 16:39:04 +08:00
|
|
|
|
<div class="toolbar-right">
|
|
|
|
|
<button class="csv-inject-button" id="csvInjectButton">
|
|
|
|
|
<img src="assets/icons/png/file.png" alt="CSV" style="width: 16px; height: 16px;">
|
|
|
|
|
CSV文件注入
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-overlay" id="csvModal">
|
|
|
|
|
<div class="modal-content">
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
<h3 class="modal-title">CSV文件注入</h3>
|
|
|
|
|
<button class="modal-close" id="modalClose">×</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>选择CSV文件</label>
|
|
|
|
|
<div class="file-input-wrapper">
|
|
|
|
|
<div class="file-input-trigger" id="fileInputTrigger">
|
|
|
|
|
点击或拖拽文件到此处
|
|
|
|
|
</div>
|
|
|
|
|
<input type="file" id="csvFileInput" accept=".csv" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="file-name" id="fileName"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>注入频率 (Hz)</label>
|
|
|
|
|
<input type="number" id="injectFrequency" min="10" max="10000" step="10" value="100" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>注入时长 (秒)</label>
|
|
|
|
|
<input type="number" id="injectDuration" min="1" max="3600" step="1" value="60" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
<button class="modal-button secondary" id="modalCancel">取消</button>
|
|
|
|
|
<button class="modal-button primary" id="modalConfirm">确认</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-06-06 11:02:12 +08:00
|
|
|
|
</div>
|
2025-04-28 12:25:20 +08:00
|
|
|
|
<div class="monitor-container">
|
2025-06-03 16:55:53 +08:00
|
|
|
|
<div class="tree-container">
|
|
|
|
|
<div class="search-box">
|
|
|
|
|
<input type="text"
|
|
|
|
|
class="search-input"
|
|
|
|
|
placeholder="搜索接口..."
|
|
|
|
|
value="${this.searchText}">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="tree-view">
|
|
|
|
|
${Object.entries(groupedInterfaces).map(([groupName, items]) => `
|
|
|
|
|
<div class="tree-group">
|
|
|
|
|
<div class="group-header" onclick="this.parentElement.querySelector('.group-content').classList.toggle('collapsed')">
|
|
|
|
|
<span class="group-icon">▼</span>
|
|
|
|
|
${groupName}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="group-content">
|
|
|
|
|
${items.map(item => `
|
|
|
|
|
<div class="interface-item" data-interfacename="${item.InterfaceName}" data-modelstructname="${item.ModelStructName}">
|
|
|
|
|
${item.InterfaceName}
|
|
|
|
|
</div>
|
|
|
|
|
`).join('')}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('')}
|
|
|
|
|
</div>
|
2025-04-28 16:41:21 +08:00
|
|
|
|
</div>
|
2025-06-03 16:55:53 +08:00
|
|
|
|
<div class="content-area">
|
2025-06-10 15:21:07 +08:00
|
|
|
|
<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>
|
2025-06-11 15:25:25 +08:00
|
|
|
|
<th style="width: 120px">注入频率(Hz)<div class="resizer"></div></th>
|
2025-06-10 15:21:07 +08:00
|
|
|
|
<th>操作</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
2025-06-11 10:59:35 +08:00
|
|
|
|
${this.tableData.map(row => {
|
|
|
|
|
return `
|
2025-06-11 15:25:25 +08:00
|
|
|
|
<tr data-interface="${row.InterfaceName}" data-struct="${row.ModelStructName}">
|
2025-06-11 10:59:35 +08:00
|
|
|
|
<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}"
|
2025-06-11 15:25:25 +08:00
|
|
|
|
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' : ''}>
|
2025-06-11 10:59:35 +08:00
|
|
|
|
</td>
|
|
|
|
|
<td>
|
|
|
|
|
<div class="action-buttons">
|
|
|
|
|
<button class="action-button plot" data-interface="${row.InterfaceName}" data-struct="${row.ModelStructName}">绘图</button>
|
2025-06-11 15:25:25 +08:00
|
|
|
|
<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>
|
2025-06-11 10:59:35 +08:00
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
`;
|
|
|
|
|
}).join('')}
|
2025-06-10 15:21:07 +08:00
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
2025-06-03 16:55:53 +08:00
|
|
|
|
</div>
|
2025-04-28 12:25:20 +08:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
|
2025-06-11 10:59:35 +08:00
|
|
|
|
const injectInputs = this.shadowRoot.querySelectorAll('.inject-input');
|
|
|
|
|
|
2025-06-03 16:55:53 +08:00
|
|
|
|
// 搜索框事件
|
|
|
|
|
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 });
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-10 15:21:07 +08:00
|
|
|
|
// 添加列宽调整功能
|
|
|
|
|
const table = this.shadowRoot.querySelector('.data-table');
|
|
|
|
|
const resizers = table.querySelectorAll('.resizer');
|
|
|
|
|
|
|
|
|
|
resizers.forEach(resizer => {
|
|
|
|
|
let startX, startWidth;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
|
2025-06-10 15:21:07 +08:00
|
|
|
|
resizer.addEventListener('mousedown', (e) => {
|
2025-06-03 16:55:53 +08:00
|
|
|
|
e.preventDefault();
|
2025-06-10 15:21:07 +08:00
|
|
|
|
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);
|
2025-06-03 16:55:53 +08:00
|
|
|
|
document.body.style.cursor = 'col-resize';
|
2025-06-10 15:21:07 +08:00
|
|
|
|
resizer.classList.add('active');
|
|
|
|
|
});
|
2025-06-03 16:55:53 +08:00
|
|
|
|
});
|
2025-06-10 15:21:07 +08:00
|
|
|
|
|
2025-06-11 10:59:35 +08:00
|
|
|
|
// 添加注入值输入框的事件处理
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-11 15:25:25 +08:00
|
|
|
|
// 添加频率输入框的事件处理
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-11 10:59:35 +08:00
|
|
|
|
// 添加注入一次按钮的事件委托
|
|
|
|
|
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}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-10 15:21:07 +08:00
|
|
|
|
// 添加删除按钮的事件委托
|
|
|
|
|
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);
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-10 15:21:07 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 添加绘图按钮的事件委托
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-06-11 15:25:25 +08:00
|
|
|
|
|
|
|
|
|
// 添加连续注入按钮的事件委托
|
|
|
|
|
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}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-06-11 16:39:04 +08:00
|
|
|
|
|
|
|
|
|
// 添加CSV文件注入相关的事件监听
|
|
|
|
|
const csvInjectButton = this.shadowRoot.getElementById('csvInjectButton');
|
|
|
|
|
const modal = this.shadowRoot.getElementById('csvModal');
|
|
|
|
|
const modalClose = this.shadowRoot.getElementById('modalClose');
|
|
|
|
|
const modalCancel = this.shadowRoot.getElementById('modalCancel');
|
|
|
|
|
const modalConfirm = this.shadowRoot.getElementById('modalConfirm');
|
|
|
|
|
const fileInput = this.shadowRoot.getElementById('csvFileInput');
|
|
|
|
|
const fileInputTrigger = this.shadowRoot.getElementById('fileInputTrigger');
|
|
|
|
|
|
|
|
|
|
if (csvInjectButton) {
|
|
|
|
|
csvInjectButton.addEventListener('click', this.handleCsvInject);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (modalClose) {
|
|
|
|
|
modalClose.addEventListener('click', this.handleModalClose);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (modalCancel) {
|
|
|
|
|
modalCancel.addEventListener('click', this.handleModalCancel);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (modalConfirm) {
|
|
|
|
|
modalConfirm.addEventListener('click', this.handleModalConfirm);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (fileInput) {
|
|
|
|
|
fileInput.addEventListener('change', this.handleFileInput);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (fileInputTrigger) {
|
|
|
|
|
fileInputTrigger.addEventListener('dragover', (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
fileInputTrigger.style.borderColor = '#1890ff';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
fileInputTrigger.addEventListener('dragleave', (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
fileInputTrigger.style.borderColor = '#d9d9d9';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
fileInputTrigger.addEventListener('drop', (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
fileInputTrigger.style.borderColor = '#d9d9d9';
|
|
|
|
|
|
|
|
|
|
const files = e.dataTransfer.files;
|
|
|
|
|
if (files.length > 0) {
|
|
|
|
|
fileInput.files = files;
|
|
|
|
|
this.handleFileInput({ target: fileInput });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-06-10 15:21:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-11 10:59:35 +08:00
|
|
|
|
/**
|
|
|
|
|
* @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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-10 15:21:07 +08:00
|
|
|
|
/**
|
|
|
|
|
* @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 })
|
|
|
|
|
});
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-10 15:21:07 +08:00
|
|
|
|
|
|
|
|
|
// 从表格数据中移除
|
|
|
|
|
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);
|
|
|
|
|
}
|
2025-06-06 11:02:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-11 16:39:04 +08:00
|
|
|
|
/**
|
|
|
|
|
* @description 处理CSV文件注入按钮点击事件
|
|
|
|
|
*/
|
|
|
|
|
handleCsvInject() {
|
|
|
|
|
const modal = this.shadowRoot.getElementById('csvModal');
|
|
|
|
|
if (modal) {
|
|
|
|
|
modal.classList.add('active');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @description 处理模态框关闭事件
|
|
|
|
|
*/
|
|
|
|
|
handleModalClose() {
|
|
|
|
|
const modal = this.shadowRoot.getElementById('csvModal');
|
|
|
|
|
if (modal) {
|
|
|
|
|
modal.classList.remove('active');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @description 处理模态框取消按钮点击事件
|
|
|
|
|
*/
|
|
|
|
|
handleModalCancel() {
|
|
|
|
|
this.handleModalClose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @description 处理模态框确认按钮点击事件
|
|
|
|
|
*/
|
|
|
|
|
async handleModalConfirm() {
|
|
|
|
|
const fileInput = this.shadowRoot.getElementById('csvFileInput');
|
|
|
|
|
const frequencyInput = this.shadowRoot.getElementById('injectFrequency');
|
|
|
|
|
const durationInput = this.shadowRoot.getElementById('injectDuration');
|
|
|
|
|
|
|
|
|
|
if (!fileInput.files.length) {
|
|
|
|
|
alert('请选择CSV文件');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const file = fileInput.files[0];
|
|
|
|
|
const frequency = parseInt(frequencyInput.value);
|
|
|
|
|
const duration = parseInt(durationInput.value);
|
|
|
|
|
|
|
|
|
|
if (!this.validateCsvFile(file)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// TODO: 实现CSV文件注入逻辑
|
|
|
|
|
console.log('CSV文件注入:', {
|
|
|
|
|
file: file.name,
|
|
|
|
|
frequency,
|
|
|
|
|
duration
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.handleModalClose();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('CSV文件注入失败:', error);
|
|
|
|
|
alert(`CSV文件注入失败: ${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @description 处理文件输入事件
|
|
|
|
|
* @param {Event} event - 文件输入事件
|
|
|
|
|
*/
|
|
|
|
|
handleFileInput(event) {
|
|
|
|
|
const file = event.target.files[0];
|
|
|
|
|
const fileName = this.shadowRoot.getElementById('fileName');
|
|
|
|
|
|
|
|
|
|
if (file) {
|
|
|
|
|
fileName.textContent = file.name;
|
|
|
|
|
} else {
|
|
|
|
|
fileName.textContent = '';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @description 验证CSV文件
|
|
|
|
|
* @param {File} file - CSV文件对象
|
|
|
|
|
* @returns {boolean} 是否验证通过
|
|
|
|
|
*/
|
|
|
|
|
validateCsvFile(file) {
|
|
|
|
|
// TODO: 实现CSV文件验证逻辑
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 11:02:12 +08:00
|
|
|
|
disconnectedCallback() {
|
2025-06-09 13:46:52 +08:00
|
|
|
|
this.isActive = false;
|
2025-06-10 15:21:07 +08:00
|
|
|
|
// 清理所有图表窗口
|
2025-06-10 15:26:31 +08:00
|
|
|
|
this.chartWindows.forEach((window, windowId) => {
|
|
|
|
|
// 触发关闭事件,确保所有清理工作都完成
|
|
|
|
|
window.dispatchEvent(new CustomEvent('close'));
|
2025-06-10 15:21:07 +08:00
|
|
|
|
window.remove();
|
|
|
|
|
});
|
|
|
|
|
this.chartWindows.clear();
|
2025-06-06 11:02:12 +08:00
|
|
|
|
// 组件销毁时清理定时器
|
|
|
|
|
this.stopDataUpdateTimer();
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-10 15:21:07 +08:00
|
|
|
|
customElements.define('data-monitor', DataMonitor);
|
|
|
|
|
|