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 = []; // 表格数据,存储已添加的接口
|
|
|
|
|
this.chart = null; // Chart.js 实例
|
|
|
|
|
this.tableHeight = 220; // 表格初始高度(px)
|
|
|
|
|
this.isResizing = false; // 是否正在拖动分隔线
|
|
|
|
|
this.startY = 0; // 拖动起始Y
|
|
|
|
|
this.startTableHeight = 0; // 拖动起始表格高度
|
|
|
|
|
this.activeChartIndexes = []; // 当前绘图的行索引
|
|
|
|
|
// 列宽数组,初始为像素
|
|
|
|
|
this.colWidths = [160, 160, 120, 180, 320]; // px
|
|
|
|
|
this.dragColIndex = null;
|
|
|
|
|
this.dragStartX = 0;
|
|
|
|
|
this.dragStartWidth = 0;
|
|
|
|
|
this._colWidthInited = false;
|
|
|
|
|
this._resizeEventBinded = false;
|
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-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
connectedCallback() {
|
2025-06-03 16:55:53 +08:00
|
|
|
|
this.loadInterfaces();
|
|
|
|
|
// 延迟多次尝试,直到拿到有效宽度
|
|
|
|
|
const tryInitColWidth = (tryCount = 0) => {
|
|
|
|
|
const section = this.shadowRoot && this.shadowRoot.querySelector('.table-section');
|
|
|
|
|
if (section && section.clientWidth > 0) {
|
|
|
|
|
const totalWidth = section.clientWidth;
|
|
|
|
|
const ratios = [0.17, 0.17, 0.13, 0.19, 0.34];
|
|
|
|
|
this.colWidths = ratios.map(r => Math.floor(totalWidth * r));
|
|
|
|
|
this._colWidthInited = true;
|
|
|
|
|
this.render();
|
|
|
|
|
} else if (tryCount < 10) {
|
|
|
|
|
setTimeout(() => tryInitColWidth(tryCount + 1), 50);
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
};
|
|
|
|
|
tryInitColWidth();
|
|
|
|
|
// 只绑定一次全局拖动事件
|
|
|
|
|
if (!this._resizeEventBinded) {
|
|
|
|
|
window.addEventListener('mousemove', this._onWindowMouseMove = (e) => {
|
|
|
|
|
if (this.isResizing) {
|
|
|
|
|
const delta = e.clientY - this.startY;
|
|
|
|
|
let newHeight = this.startTableHeight + delta;
|
|
|
|
|
const minHeight = 60;
|
|
|
|
|
const maxHeight = Math.max(120, this.offsetHeight - 180);
|
|
|
|
|
if (newHeight < minHeight) newHeight = minHeight;
|
|
|
|
|
if (newHeight > maxHeight) newHeight = maxHeight;
|
|
|
|
|
this.tableHeight = newHeight;
|
|
|
|
|
this.render();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
window.addEventListener('mouseup', this._onWindowMouseUp = () => {
|
|
|
|
|
if (this.isResizing) {
|
|
|
|
|
this.isResizing = false;
|
|
|
|
|
const divider = this.shadowRoot.querySelector('.divider');
|
|
|
|
|
if (divider) divider.classList.remove('active');
|
|
|
|
|
document.body.style.cursor = '';
|
|
|
|
|
document.body.style.userSelect = '';
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
this._resizeEventBinded = true;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
|
this.render();
|
|
|
|
|
|
|
|
|
|
// 在下一个事件循环中恢复焦点和光标位置
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
const searchInput = this.shadowRoot.querySelector('.search-input');
|
|
|
|
|
if (searchInput) {
|
|
|
|
|
searchInput.focus();
|
|
|
|
|
searchInput.setSelectionRange(this.cursorPosition, this.cursorPosition);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}, 300);
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
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)) {
|
|
|
|
|
this.tableData.push({
|
|
|
|
|
InterfaceName: item.InterfaceName,
|
|
|
|
|
ModelStructName: item.ModelStructName,
|
|
|
|
|
InjectValue: '',
|
|
|
|
|
Drawing: false,
|
2025-06-06 11:02:12 +08:00
|
|
|
|
isMonitoring: false,
|
2025-06-03 16:55:53 +08:00
|
|
|
|
color: this.getRandomColor()
|
|
|
|
|
});
|
|
|
|
|
this.renderTable();
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 11:02:12 +08:00
|
|
|
|
/**
|
|
|
|
|
* @description 初始化DDS监控服务
|
|
|
|
|
* @returns {Promise<boolean>} 初始化是否成功
|
|
|
|
|
*/
|
|
|
|
|
async initializeDDSMonitor() {
|
|
|
|
|
try {
|
|
|
|
|
// 获取当前选择的配置
|
|
|
|
|
const selection = this.getCurrentSelection();
|
|
|
|
|
const { domainId } = selection;
|
|
|
|
|
if (!domainId) {
|
|
|
|
|
throw new Error('未找到有效的域ID,请确保已选择构型并等待构型加载完成');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查DDS监控状态
|
|
|
|
|
const statusResponse = await fetch('/api/dds-monitor/status');
|
|
|
|
|
if (!statusResponse.ok) {
|
|
|
|
|
throw new Error(`获取DDS监控状态失败: ${statusResponse.status} ${statusResponse.statusText}`);
|
|
|
|
|
}
|
|
|
|
|
const statusData = await statusResponse.json();
|
|
|
|
|
|
|
|
|
|
// 如果未初始化,则初始化
|
|
|
|
|
if (!statusData.isInitialized) {
|
|
|
|
|
const initResponse = await fetch('/api/dds-monitor/initialize', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
domainId,
|
|
|
|
|
monitorId: this.monitorId
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!initResponse.ok) {
|
|
|
|
|
const errorData = await initResponse.json();
|
|
|
|
|
throw new Error(`初始化DDS监控失败: ${errorData.error || initResponse.statusText}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @description 按结构体名称分组获取接口数据
|
|
|
|
|
* @returns {Object} 按结构体名称分组的接口数据
|
|
|
|
|
*/
|
|
|
|
|
getGroupedInterfaces() {
|
|
|
|
|
return this.tableData.reduce((groups, row) => {
|
|
|
|
|
if (!groups[row.ModelStructName]) {
|
|
|
|
|
groups[row.ModelStructName] = [];
|
|
|
|
|
}
|
|
|
|
|
groups[row.ModelStructName].push(row.InterfaceName);
|
|
|
|
|
return groups;
|
|
|
|
|
}, {});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @description 估算数据缓冲区大小
|
|
|
|
|
* @param {Array<string>} interfaceNames - 接口名称数组
|
|
|
|
|
* @returns {number} 估算的缓冲区大小(字节)
|
|
|
|
|
*/
|
|
|
|
|
estimateBufferSize(interfaceNames) {
|
|
|
|
|
// 基础开销:JSON格式的开销(括号、引号、逗号等)
|
|
|
|
|
const baseOverhead = 100;
|
|
|
|
|
|
|
|
|
|
// 每个接口的估算大小
|
|
|
|
|
const interfaceSize = interfaceNames.reduce((total, name) => {
|
|
|
|
|
// 接口名称长度 + 引号 + 冒号
|
|
|
|
|
const nameOverhead = name.length + 4;
|
|
|
|
|
// 假设每个数据值平均长度为50字节(包括数字、字符串等)
|
|
|
|
|
const estimatedValueSize = 50;
|
|
|
|
|
// 逗号分隔符
|
|
|
|
|
const separatorSize = 1;
|
|
|
|
|
|
|
|
|
|
return total + nameOverhead + estimatedValueSize + separatorSize;
|
|
|
|
|
}, 0);
|
|
|
|
|
|
|
|
|
|
// 添加一些额外的缓冲空间(20%)
|
|
|
|
|
const safetyMargin = Math.ceil((baseOverhead + interfaceSize) * 0.2);
|
|
|
|
|
|
|
|
|
|
// 确保返回的大小是4KB的倍数,并设置最小值为8KB
|
|
|
|
|
const minSize = 8192;
|
|
|
|
|
const size = Math.max(minSize, Math.ceil((baseOverhead + interfaceSize + safetyMargin) / 4096) * 4096);
|
|
|
|
|
|
|
|
|
|
return size;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @description 启动数据更新定时器
|
|
|
|
|
*/
|
|
|
|
|
startDataUpdateTimer() {
|
|
|
|
|
if (this.dataUpdateTimer) {
|
|
|
|
|
clearInterval(this.dataUpdateTimer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.dataUpdateTimer = setInterval(async () => {
|
|
|
|
|
try {
|
|
|
|
|
const groupedInterfaces = this.getGroupedInterfaces();
|
|
|
|
|
|
|
|
|
|
// 对每个结构体启动监控并获取数据
|
|
|
|
|
for (const [structName, interfaceNames] of Object.entries(groupedInterfaces)) {
|
|
|
|
|
try {
|
|
|
|
|
// 启动数据监控
|
|
|
|
|
const startResponse = await fetch('/api/data-monitor/start', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({ structName })
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!startResponse.ok) {
|
|
|
|
|
throw new Error(`启动数据监控失败: ${structName}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取监控数据
|
|
|
|
|
const interfaceNamesStr = JSON.stringify(interfaceNames);
|
|
|
|
|
// 估算所需的缓冲区大小
|
|
|
|
|
const bufferSize = this.estimateBufferSize(interfaceNames);
|
|
|
|
|
|
|
|
|
|
const infoResponse = await fetch(`/api/data-monitor/info?structName=${encodeURIComponent(structName)}&interfaceName=${encodeURIComponent(interfaceNamesStr)}&bufferSize=${bufferSize}`);
|
|
|
|
|
|
|
|
|
|
if (!infoResponse.ok) {
|
|
|
|
|
throw new Error(`获取监控数据失败: ${structName}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const responseData = await infoResponse.json();
|
|
|
|
|
|
|
|
|
|
if (!responseData.success) {
|
|
|
|
|
throw new Error(`获取监控数据失败: ${responseData.message || '未知错误'}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!responseData.data) {
|
|
|
|
|
throw new Error(`获取监控数据失败: 返回数据为空`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新表格数据
|
|
|
|
|
this.tableData.forEach(row => {
|
|
|
|
|
if (row.ModelStructName === structName && responseData.data[row.InterfaceName]) {
|
|
|
|
|
row.monitorData = responseData.data[row.InterfaceName];
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} catch (structError) {
|
|
|
|
|
console.error(`处理结构体 ${structName} 时出错:`, structError);
|
|
|
|
|
// 继续处理其他结构体
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新表格显示
|
|
|
|
|
this.renderTable();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('数据更新失败:', error);
|
|
|
|
|
// 如果发生错误,停止定时器
|
|
|
|
|
this.stopDataUpdateTimer();
|
|
|
|
|
// 更新UI状态
|
|
|
|
|
const globalMonitorBtn = this.shadowRoot.getElementById('globalMonitorBtn');
|
|
|
|
|
const statusIndicator = this.shadowRoot.getElementById('statusIndicator');
|
|
|
|
|
const statusText = this.shadowRoot.getElementById('statusText');
|
|
|
|
|
|
|
|
|
|
if (globalMonitorBtn) {
|
|
|
|
|
globalMonitorBtn.textContent = '开始监控';
|
|
|
|
|
globalMonitorBtn.classList.remove('monitoring');
|
|
|
|
|
}
|
|
|
|
|
if (statusIndicator) {
|
|
|
|
|
statusIndicator.classList.remove('active');
|
|
|
|
|
statusIndicator.classList.add('error');
|
|
|
|
|
}
|
|
|
|
|
if (statusText) {
|
|
|
|
|
statusText.textContent = '监控错误';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 清空所有监控数据
|
|
|
|
|
this.tableData.forEach(row => {
|
|
|
|
|
row.isMonitoring = false;
|
|
|
|
|
row.monitorData = '';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.renderTable();
|
|
|
|
|
alert(`数据监控发生错误: ${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
}, 1000); // 每秒更新一次
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @description 停止数据更新定时器,并通知后端停止监控所有相关结构体
|
|
|
|
|
*/
|
|
|
|
|
async stopDataUpdateTimer() {
|
|
|
|
|
// 先清理前端定时器
|
|
|
|
|
if (this.dataUpdateTimer) {
|
|
|
|
|
clearInterval(this.dataUpdateTimer);
|
|
|
|
|
this.dataUpdateTimer = null;
|
|
|
|
|
}
|
|
|
|
|
// 收集所有正在监控的结构体名(去重)
|
|
|
|
|
const monitoringStructs = Array.from(new Set(
|
|
|
|
|
this.tableData
|
|
|
|
|
.filter(row => row.isMonitoring)
|
|
|
|
|
.map(row => row.ModelStructName)
|
|
|
|
|
));
|
|
|
|
|
// 并发调用后端接口,通知停止监控
|
|
|
|
|
try {
|
|
|
|
|
await Promise.all(
|
|
|
|
|
monitoringStructs.map(structName =>
|
|
|
|
|
fetch('/api/data-monitor/stop', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ structName })
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('停止后端监控时发生错误:', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 16:55:53 +08:00
|
|
|
|
/**
|
|
|
|
|
* @description 渲染表格内容
|
|
|
|
|
*/
|
|
|
|
|
renderTable() {
|
|
|
|
|
const tableBody = this.shadowRoot.querySelector('#monitor-table-body');
|
|
|
|
|
if (tableBody) {
|
|
|
|
|
tableBody.innerHTML = this.tableData.map((row, idx) => `
|
|
|
|
|
<tr>
|
|
|
|
|
<td><span class="cell-text">${row.InterfaceName}</span></td>
|
|
|
|
|
<td><span class="cell-text">${row.ModelStructName}</span></td>
|
2025-06-06 11:02:12 +08:00
|
|
|
|
<td><span class="cell-text">${row.monitorData || ''}</span></td>
|
2025-06-03 16:55:53 +08:00
|
|
|
|
<td>
|
|
|
|
|
<input type="text" class="inject-input" data-index="${idx}" value="${row.InjectValue || ''}" placeholder="注入值" style="width:90px;" />
|
|
|
|
|
</td>
|
|
|
|
|
<td style="min-width:120px; white-space:nowrap;">
|
2025-06-06 11:02:12 +08:00
|
|
|
|
<button class="action-btn chart-btn${row.Drawing ? ' drawing' : ''}" data-index="${idx}" ${!row.isMonitoring ? 'disabled' : ''}>绘图</button>
|
|
|
|
|
<button class="action-btn inject-once-btn" data-index="${idx}" ${!row.isMonitoring ? 'disabled' : ''}>单次注入</button>
|
|
|
|
|
<button class="action-btn inject-loop-btn" data-index="${idx}" ${!row.isMonitoring ? 'disabled' : ''}>连续注入</button>
|
2025-06-03 16:55:53 +08:00
|
|
|
|
<button class="action-btn delete-btn" data-index="${idx}">删除</button>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
`).join('');
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
|
|
|
|
|
// 注入值输入限制
|
|
|
|
|
this.shadowRoot.querySelectorAll('.inject-input').forEach(input => {
|
|
|
|
|
input.oninput = (e) => {
|
|
|
|
|
// 只允许数字、负号、小数点、逗号
|
|
|
|
|
let v = e.target.value.replace(/[^0-9\-.,]/g, '');
|
|
|
|
|
e.target.value = v;
|
|
|
|
|
const idx = parseInt(e.target.getAttribute('data-index'));
|
|
|
|
|
this.tableData[idx].InjectValue = v;
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 按钮事件
|
|
|
|
|
this.shadowRoot.querySelectorAll('.chart-btn').forEach(btn => {
|
|
|
|
|
btn.onclick = (e) => {
|
|
|
|
|
const idx = parseInt(btn.getAttribute('data-index'));
|
|
|
|
|
// 多选支持
|
|
|
|
|
if (this.tableData[idx].Drawing) {
|
|
|
|
|
this.tableData[idx].Drawing = false;
|
|
|
|
|
} else {
|
|
|
|
|
this.tableData[idx].Drawing = true;
|
|
|
|
|
}
|
|
|
|
|
// 更新activeChartIndexes
|
|
|
|
|
this.activeChartIndexes = this.tableData.map((row, i) => row.Drawing ? i : null).filter(i => i !== null);
|
|
|
|
|
this.renderTable();
|
|
|
|
|
this.updateChart();
|
|
|
|
|
};
|
|
|
|
|
});
|
2025-06-06 11:02:12 +08:00
|
|
|
|
|
2025-06-03 16:55:53 +08:00
|
|
|
|
this.shadowRoot.querySelectorAll('.delete-btn').forEach(btn => {
|
|
|
|
|
btn.onclick = (e) => {
|
|
|
|
|
const idx = parseInt(btn.getAttribute('data-index'));
|
|
|
|
|
this.tableData.splice(idx, 1);
|
|
|
|
|
// 删除后同步更新activeChartIndexes
|
|
|
|
|
this.activeChartIndexes = this.tableData.map((row, i) => row.Drawing ? i : null).filter(i => i !== null);
|
|
|
|
|
this.renderTable();
|
|
|
|
|
this.updateChart();
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @description 初始化图表
|
|
|
|
|
*/
|
|
|
|
|
initChart() {
|
|
|
|
|
const chartElement = this.shadowRoot.querySelector('#monitor-chart');
|
|
|
|
|
if (!chartElement) return;
|
|
|
|
|
if (typeof Chart === 'undefined') return;
|
|
|
|
|
if (this.chart) {
|
|
|
|
|
this.chart.destroy();
|
|
|
|
|
this.chart = null;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
const ctx = chartElement.getContext('2d');
|
|
|
|
|
this.chart = new Chart(ctx, {
|
|
|
|
|
type: 'line',
|
|
|
|
|
data: {
|
|
|
|
|
labels: [],
|
|
|
|
|
datasets: []
|
|
|
|
|
},
|
|
|
|
|
options: {
|
|
|
|
|
responsive: true,
|
|
|
|
|
maintainAspectRatio: false,
|
|
|
|
|
scales: {
|
|
|
|
|
y: { beginAtZero: true }
|
|
|
|
|
},
|
|
|
|
|
animation: false,
|
|
|
|
|
plugins: {
|
|
|
|
|
legend: { display: true },
|
|
|
|
|
tooltip: { enabled: false }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 16:55:53 +08:00
|
|
|
|
/**
|
|
|
|
|
* @description 获取随机颜色
|
|
|
|
|
* @returns {string} 颜色字符串
|
|
|
|
|
*/
|
|
|
|
|
getRandomColor() {
|
|
|
|
|
const letters = '0123456789ABCDEF';
|
|
|
|
|
let color = '#';
|
|
|
|
|
for (let i = 0; i < 6; i++) {
|
|
|
|
|
color += letters[Math.floor(Math.random() * 16)];
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
return color;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @description 更新图表,支持多条曲线
|
|
|
|
|
*/
|
|
|
|
|
updateChart() {
|
|
|
|
|
if (!this.chart) return;
|
|
|
|
|
if (!this.activeChartIndexes || this.activeChartIndexes.length === 0) {
|
|
|
|
|
this.chart.data.labels = [];
|
|
|
|
|
this.chart.data.datasets = [];
|
|
|
|
|
this.chart.update('none');
|
|
|
|
|
return;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
// 多条曲线
|
|
|
|
|
const labels = Array.from({length: 20}, (_, i) => i+1);
|
|
|
|
|
this.chart.data.labels = labels;
|
|
|
|
|
this.chart.data.datasets = this.activeChartIndexes.map(idx => {
|
|
|
|
|
const row = this.tableData[idx];
|
|
|
|
|
return {
|
|
|
|
|
label: row.InterfaceName,
|
|
|
|
|
data: Array.from({length: 20}, () => (Math.random()*100).toFixed(2)),
|
|
|
|
|
borderColor: row.color,
|
|
|
|
|
fill: false
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
this.chart.update('none');
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-04-28 12:25:20 +08:00
|
|
|
|
render() {
|
2025-06-03 16:55:53 +08:00
|
|
|
|
// 计算所有列宽之和
|
|
|
|
|
const totalColWidth = this.colWidths.reduce((a, b) => a + b, 0);
|
|
|
|
|
let tableWidthStyle = '';
|
|
|
|
|
const section = this.shadowRoot && this.shadowRoot.querySelector('.table-section');
|
|
|
|
|
const containerWidth = section ? section.clientWidth : 940;
|
|
|
|
|
if (totalColWidth < containerWidth) {
|
|
|
|
|
tableWidthStyle = 'width:100%;';
|
|
|
|
|
} else {
|
|
|
|
|
tableWidthStyle = 'width:max-content;';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 首次渲染时动态分配列宽(已移至connectedCallback,防止死循环)
|
|
|
|
|
// 按ModelStructName分组
|
|
|
|
|
const groupedInterfaces = this.filteredInterfaces.reduce((groups, item) => {
|
|
|
|
|
const group = groups[item.ModelStructName] || [];
|
|
|
|
|
group.push(item);
|
|
|
|
|
groups[item.ModelStructName] = group;
|
|
|
|
|
return groups;
|
|
|
|
|
}, {});
|
|
|
|
|
|
2025-04-28 12:25:20 +08:00
|
|
|
|
this.shadowRoot.innerHTML = `
|
|
|
|
|
<style>
|
|
|
|
|
:host {
|
|
|
|
|
display: block;
|
|
|
|
|
height: 100%;
|
|
|
|
|
overflow: auto;
|
|
|
|
|
padding: 16px;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.global-monitor-btn {
|
|
|
|
|
padding: 6px 16px;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
background: #1890ff;
|
|
|
|
|
color: white;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.global-monitor-btn:hover {
|
|
|
|
|
background: #40a9ff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.global-monitor-btn.monitoring {
|
|
|
|
|
background: #ff4d4f;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.global-monitor-btn.monitoring:hover {
|
|
|
|
|
background: #ff7875;
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-28 12:25:20 +08:00
|
|
|
|
.monitor-container {
|
|
|
|
|
background-color: white;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
|
|
|
padding: 16px;
|
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-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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tree-group {
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.group-content {
|
|
|
|
|
margin-left: 20px;
|
|
|
|
|
display: block;
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.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
|
|
|
|
|
|
|
|
|
.table-section {
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-radius: 8px 8px 0 0;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
|
|
|
|
padding: 0;
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
max-height: 70vh;
|
|
|
|
|
min-height: 60px;
|
|
|
|
|
height: ${this.tableHeight}px;
|
|
|
|
|
transition: height 0.1s;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
.table-scroll-x {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
overflow-y: hidden;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
.table-header-fixed {
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
width: fit-content;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
.table-header-fixed .monitor-table {
|
|
|
|
|
width: max-content;
|
|
|
|
|
min-width: 100%;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
.table-body-scroll {
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
width: 100%;
|
|
|
|
|
overflow-x: hidden;
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
max-height: calc(100% - 1px);
|
|
|
|
|
}
|
|
|
|
|
.table-body-scroll .monitor-table {
|
|
|
|
|
width: max-content;
|
|
|
|
|
min-width: 100%;
|
|
|
|
|
}
|
|
|
|
|
.monitor-table {
|
|
|
|
|
width: max-content;
|
|
|
|
|
min-width: 100%;
|
|
|
|
|
border-collapse: separate;
|
|
|
|
|
border-spacing: 0;
|
|
|
|
|
table-layout: fixed;
|
|
|
|
|
min-width: unset;
|
|
|
|
|
}
|
|
|
|
|
.monitor-table th, .monitor-table td {
|
|
|
|
|
padding: 8px;
|
|
|
|
|
border-bottom: 1px solid #e0e0e0;
|
|
|
|
|
text-align: left;
|
|
|
|
|
background: #fff;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
position: relative;
|
|
|
|
|
max-width: 0;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
.cell-text {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
white-space: nowrap;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
vertical-align: middle;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
.monitor-table input,
|
|
|
|
|
.monitor-table button {
|
|
|
|
|
min-width: 0;
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
white-space: nowrap;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
.monitor-table th {
|
|
|
|
|
background: #f5f5f5;
|
|
|
|
|
position: sticky;
|
|
|
|
|
top: 0;
|
|
|
|
|
z-index: 2;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
/* 竖线:表头和内容区 */
|
|
|
|
|
.monitor-table td:not(:last-child)::after {
|
|
|
|
|
content: '';
|
|
|
|
|
position: absolute;
|
|
|
|
|
right: 0;
|
|
|
|
|
top: 20%;
|
|
|
|
|
width: 1px;
|
|
|
|
|
height: 60%;
|
|
|
|
|
background: #e0e0e0;
|
|
|
|
|
z-index: 1;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
.monitor-table th.th-resize-active::after {
|
|
|
|
|
background: #1890ff;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
|
|
|
|
|
.divider {
|
|
|
|
|
height: 12px;
|
|
|
|
|
background: transparent;
|
|
|
|
|
cursor: row-resize;
|
|
|
|
|
width: 100%;
|
|
|
|
|
position: relative;
|
|
|
|
|
z-index: 2;
|
|
|
|
|
transition: background 0.2s;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
justify-content: center;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
.divider-bar {
|
|
|
|
|
width: 60px;
|
|
|
|
|
height: 5px;
|
|
|
|
|
background: #c0c4cc;
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
|
|
|
|
|
transition: background 0.2s, box-shadow 0.2s;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
.divider:hover .divider-bar,
|
|
|
|
|
.divider.active .divider-bar {
|
|
|
|
|
background: #1890ff;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(24,144,255,0.15);
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
|
|
|
|
|
.chart-section {
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-radius: 0 0 8px 8px;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
|
|
|
|
padding: 12px;
|
|
|
|
|
flex: 1;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
min-height: 120px;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
overflow: hidden;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
.chart-title {
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
margin-bottom: 8px;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
.chart-container {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-height: 120px;
|
|
|
|
|
position: relative;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
#monitor-chart {
|
|
|
|
|
width: 100% !important;
|
|
|
|
|
height: 100% !important;
|
|
|
|
|
display: block;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
|
|
|
|
|
.action-btn {
|
|
|
|
|
margin: 0 2px;
|
|
|
|
|
padding: 3px 8px;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
background: #f0f0f0;
|
2025-04-28 12:25:20 +08:00
|
|
|
|
color: #333;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
font-size: 13px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: background 0.2s;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
.action-btn:hover {
|
|
|
|
|
background: #e6f7ff;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
.chart-btn.drawing {
|
|
|
|
|
background: #1890ff;
|
|
|
|
|
color: #fff;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
|
|
|
|
|
.th-resize {
|
|
|
|
|
position: absolute;
|
|
|
|
|
right: 0;
|
|
|
|
|
top: 0;
|
|
|
|
|
width: 12px;
|
|
|
|
|
height: 100%;
|
|
|
|
|
cursor: col-resize;
|
|
|
|
|
z-index: 10;
|
|
|
|
|
user-select: none;
|
2025-04-28 16:41:21 +08:00
|
|
|
|
display: flex;
|
2025-06-03 16:55:53 +08:00
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
}
|
|
|
|
|
.th-resize::after {
|
|
|
|
|
content: '';
|
|
|
|
|
position: absolute;
|
|
|
|
|
right: 0;
|
|
|
|
|
top: 20%;
|
|
|
|
|
width: 1px;
|
|
|
|
|
height: 60%;
|
|
|
|
|
background: #e0e0e0;
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
transition: background 0.2s;
|
|
|
|
|
}
|
|
|
|
|
.th-resize:hover::after,
|
|
|
|
|
.th-resize.active::after {
|
|
|
|
|
background: #1890ff;
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
2025-06-06 11:02:12 +08:00
|
|
|
|
.monitor-btn.monitoring {
|
|
|
|
|
background: #ff4d4f;
|
|
|
|
|
color: #fff;
|
|
|
|
|
}
|
|
|
|
|
.action-btn:disabled {
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
}
|
2025-04-28 12:25:20 +08:00
|
|
|
|
</style>
|
2025-06-06 11:02:12 +08:00
|
|
|
|
<div class="toolbar">
|
|
|
|
|
<div class="toolbar-left">
|
|
|
|
|
<button class="global-monitor-btn" id="globalMonitorBtn">开始监控</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="toolbar-right">
|
|
|
|
|
<div class="monitor-status">
|
|
|
|
|
<div class="status-indicator" id="statusIndicator"></div>
|
|
|
|
|
<span id="statusText">未监控</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-04-28 12:25:20 +08:00
|
|
|
|
<div class="monitor-container">
|
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">
|
|
|
|
|
<div class="table-section">
|
|
|
|
|
<div class="table-header-fixed">
|
|
|
|
|
<table class="monitor-table" style="${tableWidthStyle}">
|
|
|
|
|
<colgroup>
|
|
|
|
|
<col style="width:${this.colWidths[0]}px">
|
|
|
|
|
<col style="width:${this.colWidths[1]}px">
|
|
|
|
|
<col style="width:${this.colWidths[2]}px">
|
|
|
|
|
<col style="width:${this.colWidths[3]}px">
|
|
|
|
|
<col style="width:${this.colWidths[4]}px">
|
|
|
|
|
</colgroup>
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>接口名称<div class="th-resize" data-col="0"></div></th>
|
|
|
|
|
<th>结构体名<div class="th-resize" data-col="1"></div></th>
|
|
|
|
|
<th>数据<div class="th-resize" data-col="2"></div></th>
|
|
|
|
|
<th>注入值<div class="th-resize" data-col="3"></div></th>
|
|
|
|
|
<th>操作<div class="th-resize" data-col="4"></div></th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="table-body-scroll">
|
|
|
|
|
<table class="monitor-table" style="${tableWidthStyle}">
|
|
|
|
|
<colgroup>
|
|
|
|
|
<col style="width:${this.colWidths[0]}px">
|
|
|
|
|
<col style="width:${this.colWidths[1]}px">
|
|
|
|
|
<col style="width:${this.colWidths[2]}px">
|
|
|
|
|
<col style="width:${this.colWidths[3]}px">
|
|
|
|
|
<col style="width:${this.colWidths[4]}px">
|
|
|
|
|
</colgroup>
|
|
|
|
|
<tbody id="monitor-table-body">
|
|
|
|
|
<!-- 表格内容 -->
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="divider"><div class="divider-bar"></div></div>
|
|
|
|
|
<div class="chart-section">
|
|
|
|
|
<div class="chart-title">数据监控图表</div>
|
|
|
|
|
<div class="chart-container">
|
|
|
|
|
<canvas id="monitor-chart"></canvas>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-04-28 12:25:20 +08:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
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 });
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 渲染表格内容
|
|
|
|
|
this.renderTable();
|
|
|
|
|
|
|
|
|
|
// 初始化图表
|
|
|
|
|
this.initChart();
|
|
|
|
|
|
|
|
|
|
// 初始绘图
|
|
|
|
|
this.updateChart();
|
|
|
|
|
|
|
|
|
|
// 分隔线拖动事件
|
|
|
|
|
const divider = this.shadowRoot.querySelector('.divider');
|
|
|
|
|
if (divider) {
|
|
|
|
|
divider.onmousedown = (e) => {
|
|
|
|
|
this.isResizing = true;
|
|
|
|
|
this.startY = e.clientY;
|
|
|
|
|
this.startTableHeight = this.tableHeight;
|
|
|
|
|
divider.classList.add('active');
|
|
|
|
|
document.body.style.cursor = 'row-resize';
|
|
|
|
|
document.body.style.userSelect = 'none';
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 列宽拖动事件
|
|
|
|
|
this.shadowRoot.querySelectorAll('.th-resize').forEach(handle => {
|
|
|
|
|
handle.onmousedown = (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
this.dragColIndex = parseInt(handle.getAttribute('data-col'));
|
|
|
|
|
this.dragStartX = e.clientX;
|
|
|
|
|
this.dragStartWidth = this.colWidths[this.dragColIndex];
|
|
|
|
|
document.body.style.cursor = 'col-resize';
|
|
|
|
|
document.body.style.userSelect = 'none';
|
|
|
|
|
handle.classList.add('active');
|
|
|
|
|
// 高亮竖线
|
|
|
|
|
const th = handle.parentElement;
|
|
|
|
|
if (th) th.classList.add('th-resize-active');
|
|
|
|
|
};
|
|
|
|
|
handle.onmouseup = () => {
|
|
|
|
|
handle.classList.remove('active');
|
|
|
|
|
const th = handle.parentElement;
|
|
|
|
|
if (th) th.classList.remove('th-resize-active');
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
window.onmousemove = (e) => {
|
|
|
|
|
if (this.dragColIndex !== null) {
|
|
|
|
|
const delta = e.clientX - this.dragStartX;
|
|
|
|
|
let newWidth = this.dragStartWidth + delta;
|
|
|
|
|
if (newWidth < 60) newWidth = 60;
|
|
|
|
|
this.colWidths[this.dragColIndex] = newWidth;
|
|
|
|
|
this.render();
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
};
|
|
|
|
|
window.onmouseup = () => {
|
|
|
|
|
if (this.dragColIndex !== null) {
|
|
|
|
|
// 移除所有th-resize的active
|
|
|
|
|
this.shadowRoot.querySelectorAll('.th-resize').forEach(h => h.classList.remove('active'));
|
|
|
|
|
// 移除所有th的高亮
|
|
|
|
|
this.shadowRoot.querySelectorAll('.monitor-table th').forEach(th => th.classList.remove('th-resize-active'));
|
|
|
|
|
this.dragColIndex = null;
|
|
|
|
|
document.body.style.cursor = '';
|
|
|
|
|
document.body.style.userSelect = '';
|
2025-04-28 16:41:21 +08:00
|
|
|
|
}
|
2025-06-03 16:55:53 +08:00
|
|
|
|
};
|
2025-06-06 11:02:12 +08:00
|
|
|
|
|
|
|
|
|
// 绑定全局监控按钮事件
|
|
|
|
|
const globalMonitorBtn = this.shadowRoot.getElementById('globalMonitorBtn');
|
|
|
|
|
const statusIndicator = this.shadowRoot.getElementById('statusIndicator');
|
|
|
|
|
const statusText = this.shadowRoot.getElementById('statusText');
|
|
|
|
|
|
|
|
|
|
globalMonitorBtn.addEventListener('click', async () => {
|
|
|
|
|
try {
|
|
|
|
|
if (!globalMonitorBtn.classList.contains('monitoring')) {
|
|
|
|
|
// 开始监控
|
|
|
|
|
const ddsInitialized = await this.initializeDDSMonitor();
|
|
|
|
|
if (!ddsInitialized) {
|
|
|
|
|
throw new Error('DDS监控初始化失败');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新所有行的监控状态
|
|
|
|
|
this.tableData.forEach(row => {
|
|
|
|
|
row.isMonitoring = true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 启动数据更新定时器
|
|
|
|
|
this.startDataUpdateTimer();
|
|
|
|
|
|
|
|
|
|
// 更新状态
|
|
|
|
|
globalMonitorBtn.textContent = '停止监控';
|
|
|
|
|
globalMonitorBtn.classList.add('monitoring');
|
|
|
|
|
statusIndicator.classList.add('active');
|
|
|
|
|
statusText.textContent = '监控中';
|
|
|
|
|
this.renderTable();
|
|
|
|
|
} else {
|
|
|
|
|
// 停止监控
|
|
|
|
|
// 停止数据更新定时器
|
|
|
|
|
await this.stopDataUpdateTimer();
|
|
|
|
|
|
|
|
|
|
// 更新所有行的监控状态
|
|
|
|
|
this.tableData.forEach(row => {
|
|
|
|
|
row.isMonitoring = false;
|
|
|
|
|
row.monitorData = ''; // 清空监控数据
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 更新状态
|
|
|
|
|
globalMonitorBtn.textContent = '开始监控';
|
|
|
|
|
globalMonitorBtn.classList.remove('monitoring');
|
|
|
|
|
statusIndicator.classList.remove('active');
|
|
|
|
|
statusText.textContent = '未监控';
|
|
|
|
|
this.renderTable();
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
statusIndicator.classList.add('error');
|
|
|
|
|
statusText.textContent = '监控错误';
|
|
|
|
|
alert(error.message);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
disconnectedCallback() {
|
|
|
|
|
// 组件销毁时清理定时器
|
|
|
|
|
this.stopDataUpdateTimer();
|
2025-04-28 12:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
customElements.define('data-monitor', DataMonitor);
|