diff --git a/Release/database/XNSim.db b/Release/database/XNSim.db index d6b2cbd..6ba44ec 100644 Binary files a/Release/database/XNSim.db and b/Release/database/XNSim.db differ diff --git a/XNSimHtml/components/FloatingChartWindow.css b/XNSimHtml/components/FloatingChartWindow.css new file mode 100644 index 0000000..370d5af --- /dev/null +++ b/XNSimHtml/components/FloatingChartWindow.css @@ -0,0 +1,106 @@ +.floating-window { + position: fixed; + background: white; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + display: flex; + flex-direction: column; + overflow: hidden; + z-index: 1000; + transition: all 0.3s ease; +} + +.floating-window.minimized { + transform: translateY(calc(100% - 40px)); + border-radius: 8px 8px 0 0; +} + +.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 #e0e0e0; + height: 40px; +} + +.window-title { + font-size: 14px; + font-weight: 500; + color: #333; +} + +.window-controls { + display: flex; + gap: 8px; +} + +.window-control-button { + background: none; + border: none; + font-size: 16px; + color: #666; + cursor: pointer; + padding: 0 4px; + line-height: 1; + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 4px; +} + +.window-control-button:hover { + background: rgba(0, 0, 0, 0.05); +} + +.minimize-button:hover { + color: #1890ff; +} + +.close-button:hover { + color: #ff4d4f; +} + +.window-content { + flex: 1; + padding: 12px; + position: relative; + min-height: 200px; + transition: height 0.3s ease; +} + +.window-content.minimized { + height: 0; + padding: 0; + overflow: hidden; +} + +.window-content canvas { + width: 100% !important; + height: 100% !important; +} + +.resize-handle { + position: absolute; + right: 0; + bottom: 0; + width: 20px; + height: 20px; + cursor: se-resize; +} + +.resize-handle::after { + content: ''; + position: absolute; + right: 4px; + bottom: 4px; + width: 8px; + height: 8px; + border-right: 2px solid #ccc; + border-bottom: 2px solid #ccc; +} \ No newline at end of file diff --git a/XNSimHtml/components/FloatingChartWindow.js b/XNSimHtml/components/FloatingChartWindow.js new file mode 100644 index 0000000..554e2da --- /dev/null +++ b/XNSimHtml/components/FloatingChartWindow.js @@ -0,0 +1,570 @@ +// 确保 Chart.js 已加载 +if (!window.Chart) { + const script = document.createElement('script'); + script.src = '../chart.min.js'; + document.head.appendChild(script); +} + +class FloatingChartWindow extends HTMLElement { + // 添加静态计数器 + static zIndexCounter = 1; + + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + + // 初始化状态 + this.position = { x: 100, y: 100 }; + this.size = { width: 400, height: 300 }; + this.isDragging = false; + this.dragOffset = { x: 0, y: 0 }; + this.isResizing = false; + this.resizeStart = { x: 0, y: 0 }; + this.isMinimized = false; + this.chartInstance = null; + this.zIndex = FloatingChartWindow.zIndexCounter++; + + // 存储初始属性值 + this._title = this.getAttribute('title') || '图表窗口'; + this._initialPosition = this.getAttribute('initial-position') ? + JSON.parse(this.getAttribute('initial-position')) : { x: 100, y: 100 }; + this._initialSize = this.getAttribute('initial-size') ? + JSON.parse(this.getAttribute('initial-size')) : { width: 400, height: 300 }; + this._chartData = this.getAttribute('chart-data') ? + JSON.parse(this.getAttribute('chart-data')) : { labels: [], datasets: [] }; + this._chartOptions = this.getAttribute('chart-options') ? + JSON.parse(this.getAttribute('chart-options')) : {}; + + // 绑定方法 + this.handleMouseDown = this.handleMouseDown.bind(this); + this.handleMouseMove = this.handleMouseMove.bind(this); + this.handleMouseUp = this.handleMouseUp.bind(this); + this.handleResizeStart = this.handleResizeStart.bind(this); + this.handleResize = this.handleResize.bind(this); + this.handleResizeEnd = this.handleResizeEnd.bind(this); + this.handleMinimize = this.handleMinimize.bind(this); + this.handleClose = this.handleClose.bind(this); + } + + static get observedAttributes() { + return ['title', 'initial-position', 'initial-size', 'chart-data', 'chart-options', 'z-index']; + } + + connectedCallback() { + this.render(); + this.initChart(); + this.addEventListeners(); + + // 应用初始属性值 + this.updateTitle(this._title); + this.position = this._initialPosition; + this.size = this._initialSize; + this.updatePosition(); + this.updateSize(); + this.updateChartData(this._chartData); + this.updateChartOptions(this._chartOptions); + this.updateZIndex(this.zIndex); + + // 触发 connected 事件 + this.dispatchEvent(new CustomEvent('connected')); + } + + disconnectedCallback() { + this.removeEventListeners(); + if (this.chartInstance) { + this.chartInstance.destroy(); + } + } + + attributeChangedCallback(name, oldValue, newValue) { + if (oldValue === newValue) return; + + // 存储新的属性值 + switch (name) { + case 'title': + this._title = newValue; + break; + case 'initial-position': + this._initialPosition = JSON.parse(newValue); + break; + case 'initial-size': + this._initialSize = JSON.parse(newValue); + break; + case 'chart-data': + this._chartData = JSON.parse(newValue); + break; + case 'chart-options': + this._chartOptions = JSON.parse(newValue); + break; + case 'z-index': + this.zIndex = parseInt(newValue); + break; + } + + // 如果组件已经渲染,则更新相应的值 + if (this.shadowRoot) { + switch (name) { + case 'title': + this.updateTitle(this._title); + break; + case 'initial-position': + this.position = this._initialPosition; + this.updatePosition(); + break; + case 'initial-size': + this.size = this._initialSize; + this.updateSize(); + break; + case 'chart-data': + this.updateChartData(this._chartData); + break; + case 'chart-options': + this.updateChartOptions(this._chartOptions); + break; + case 'z-index': + this.updateZIndex(this.zIndex); + break; + } + } + } + + render() { + const style = document.createElement('style'); + style.textContent = ` + .floating-window { + position: fixed; + background: white; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + display: flex; + flex-direction: column; + overflow: hidden; + } + + .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; + flex-shrink: 0; + } + + .window-title { + font-size: 14px; + font-weight: 500; + color: #262626; + } + + .window-controls { + display: flex; + gap: 8px; + } + + .window-control-button { + width: 24px; + height: 24px; + border: none; + background: transparent; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + color: #666; + border-radius: 4px; + transition: all 0.3s; + } + + .window-control-button:hover { + background: rgba(0, 0, 0, 0.05); + color: #262626; + } + + .window-control-button.close-button:hover { + background: #ff4d4f; + color: white; + } + + .window-content { + flex: 1; + padding: 12px; + position: relative; + min-height: 0; + display: flex; + flex-direction: column; + } + + .window-content.minimized { + display: none; + } + + .chart-container { + flex: 1; + position: relative; + min-height: 0; + } + + canvas { + width: 100% !important; + height: 100% !important; + } + + .resize-handle { + position: absolute; + right: 0; + bottom: 0; + width: 10px; + height: 10px; + cursor: se-resize; + } + + .resize-handle::after { + content: ''; + position: absolute; + right: 2px; + bottom: 2px; + width: 6px; + height: 6px; + border-right: 2px solid #ccc; + border-bottom: 2px solid #ccc; + } + `; + + const template = document.createElement('template'); + template.innerHTML = ` +
+
+ ${this.getAttribute('title') || '图表窗口'} +
+ + +
+
+
+
+ +
+
+
+
+ `; + + this.shadowRoot.appendChild(style); + this.shadowRoot.appendChild(template.content.cloneNode(true)); + + // 更新位置和大小 + this.updatePosition(); + this.updateSize(); + } + + initChart() { + const canvas = this.shadowRoot.querySelector('canvas'); + const ctx = canvas.getContext('2d'); + + const chartData = JSON.parse(this.getAttribute('chart-data') || '{"labels":[],"datasets":[]}'); + const chartOptions = JSON.parse(this.getAttribute('chart-options') || '{}'); + + // 基础配置 + const baseOptions = { + responsive: true, + maintainAspectRatio: false, + animation: false, + elements: { + point: { + radius: 0 + }, + line: { + tension: 0 + } + }, + scales: { + y: { + beginAtZero: false, + ticks: { + callback: function(value) { + // 如果数值的绝对值大于1000或小于0.1,使用科学计数法 + if (Math.abs(value) > 1000 || (Math.abs(value) < 0.1 && value !== 0)) { + return value.toExponential(1); + } + // 否则使用普通数字,保留一位小数 + return value.toFixed(1); + }, + maxTicksLimit: 8 // 限制刻度数量 + } + }, + x: { + display: true, + ticks: { + callback: function(value) { + // 如果数值大于1000,使用科学计数法 + if (value > 1000) { + return value.toExponential(1); + } + return value; + } + } + } + }, + plugins: { + legend: { + display: true, + position: 'top' + }, + tooltip: { + enabled: true, + mode: 'index', + intersect: false, + callbacks: { + label: function(context) { + const value = context.raw; + // 始终使用普通数字格式,保留一位小数 + return `${context.dataset.label}: ${value.toFixed(1)}`; + } + } + } + } + }; + + // 深度合并配置 + const finalOptions = { + ...baseOptions, + scales: { + y: { + ...baseOptions.scales.y, + ...(chartOptions.scales?.y || {}), + ticks: { + ...baseOptions.scales.y.ticks, + ...(chartOptions.scales?.y?.ticks || {}) + } + }, + x: { + ...baseOptions.scales.x, + ...(chartOptions.scales?.x || {}), + ticks: { + ...baseOptions.scales.x.ticks, + ...(chartOptions.scales?.x?.ticks || {}) + } + } + }, + plugins: { + ...baseOptions.plugins, + ...(chartOptions.plugins || {}), + tooltip: { + ...baseOptions.plugins.tooltip, + ...(chartOptions.plugins?.tooltip || {}), + callbacks: { + ...baseOptions.plugins.tooltip.callbacks, + ...(chartOptions.plugins?.tooltip?.callbacks || {}) + } + } + } + }; + + this.chartInstance = new Chart(ctx, { + type: 'line', + data: chartData, + options: finalOptions + }); + } + + addEventListeners() { + const header = this.shadowRoot.querySelector('.window-header'); + const minimizeButton = this.shadowRoot.querySelector('.minimize-button'); + const closeButton = this.shadowRoot.querySelector('.close-button'); + const resizeHandle = this.shadowRoot.querySelector('.resize-handle'); + + header.addEventListener('mousedown', this.handleMouseDown); + minimizeButton.addEventListener('click', this.handleMinimize); + closeButton.addEventListener('click', this.handleClose); + resizeHandle.addEventListener('mousedown', this.handleResizeStart); + + document.addEventListener('mousemove', this.handleMouseMove); + document.addEventListener('mouseup', this.handleMouseUp); + } + + removeEventListeners() { + document.removeEventListener('mousemove', this.handleMouseMove); + document.removeEventListener('mouseup', this.handleMouseUp); + } + + handleMouseDown(e) { + // 检查点击是否在标题栏或其子元素上 + const header = this.shadowRoot.querySelector('.window-header'); + if (header && (e.target === header || header.contains(e.target)) && !this.isMinimized) { + this.isDragging = true; + this.dragOffset = { + x: e.clientX - this.position.x, + y: e.clientY - this.position.y + }; + // 点击标题栏时置顶 + const window = this.shadowRoot.querySelector('.floating-window'); + if (window) { + this.zIndex = FloatingChartWindow.zIndexCounter++; + window.style.zIndex = this.zIndex; + } + } + } + + handleMouseMove(e) { + if (this.isDragging) { + // 限制拖动范围在当前页面区域 + const minX = 300; // 左侧工具栏宽度 + const minY = 100; // 顶部标签页栏高度 + + this.position = { + x: Math.max(minX, e.clientX - this.dragOffset.x), + y: Math.max(minY, e.clientY - this.dragOffset.y) + }; + this.updatePosition(); + } else if (this.isResizing && !this.isMinimized) { + const deltaX = e.clientX - this.resizeStart.x; + const deltaY = e.clientY - this.resizeStart.y; + + this.size = { + width: Math.max(300, this.size.width + deltaX), + height: Math.max(200, this.size.height + deltaY) + }; + + this.resizeStart = { + x: e.clientX, + y: e.clientY + }; + + this.updateSize(); + } + } + + handleMouseUp() { + this.isDragging = false; + this.isResizing = false; + } + + handleResizeStart(e) { + this.isResizing = true; + this.resizeStart = { + x: e.clientX, + y: e.clientY + }; + } + + handleResize(e) { + if (this.isResizing && !this.isMinimized) { + const deltaX = e.clientX - this.resizeStart.x; + const deltaY = e.clientY - this.resizeStart.y; + + this.size = { + width: Math.max(300, this.size.width + deltaX), + height: Math.max(200, this.size.height + deltaY) + }; + + this.resizeStart = { + x: e.clientX, + y: e.clientY + }; + + this.updateSize(); + + // 更新图表大小 + if (this.chartInstance) { + this.chartInstance.resize(); + } + } + } + + handleResizeEnd() { + this.isResizing = false; + } + + handleMinimize() { + this.isMinimized = !this.isMinimized; + const content = this.shadowRoot.querySelector('.window-content'); + const minimizeButton = this.shadowRoot.querySelector('.minimize-button'); + + if (this.isMinimized) { + content.classList.add('minimized'); + minimizeButton.textContent = '□'; + } else { + content.classList.remove('minimized'); + minimizeButton.textContent = '−'; + } + } + + handleClose() { + this.dispatchEvent(new CustomEvent('close')); + this.remove(); + } + + updatePosition() { + const window = this.shadowRoot.querySelector('.floating-window'); + window.style.left = `${this.position.x}px`; + window.style.top = `${this.position.y}px`; + } + + updateSize() { + const window = this.shadowRoot.querySelector('.floating-window'); + window.style.width = `${this.size.width}px`; + window.style.height = `${this.size.height}px`; + + // 强制图表重新渲染 + if (this.chartInstance) { + this.chartInstance.resize(); + } + } + + updateTitle(title) { + const titleElement = this.shadowRoot.querySelector('.window-title'); + titleElement.textContent = title; + } + + updateChartData(data) { + if (this.chartInstance) { + // 更新数据 + this.chartInstance.data = data; + + // 强制更新配置 + const options = this.chartInstance.options; + options.scales.y.ticks.callback = function(value) { + if (Math.abs(value) > 1000 || (Math.abs(value) < 0.1 && value !== 0)) { + return value.toExponential(1); + } + return value.toFixed(1); + }; + + options.scales.x.ticks.callback = function(value) { + if (value > 1000) { + return value.toExponential(1); + } + return value; + }; + + options.plugins.tooltip.callbacks.label = function(context) { + const value = context.raw; + // 始终使用普通数字格式,保留一位小数 + return `${context.dataset.label}: ${value.toFixed(1)}`; + }; + + // 更新图表 + this.chartInstance.update('none'); + } + } + + updateChartOptions(options) { + if (this.chartInstance) { + Object.assign(this.chartInstance.options, options); + this.chartInstance.update('none'); + } + } + + updateZIndex(zIndex) { + const window = this.shadowRoot.querySelector('.floating-window'); + if (window) { + window.style.zIndex = zIndex; + } + } +} + +customElements.define('floating-chart-window', FloatingChartWindow); \ No newline at end of file diff --git a/XNSimHtml/components/data-monitor.js b/XNSimHtml/components/data-monitor.js index b62e000..f41e406 100644 --- a/XNSimHtml/components/data-monitor.js +++ b/XNSimHtml/components/data-monitor.js @@ -13,67 +13,29 @@ class DataMonitor extends HTMLElement { 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; this.monitorId = `data_monitor_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; // 添加监控器ID this.dataUpdateTimer = null; // 数据更新定时器 this.isActive = false; // 组件是否激活 + this.chartWindows = new Map(); // 存储打开的图表窗口 + + // 绑定方法 + this.handleSearch = this.handleSearch.bind(this); + this.handleTreeItemDblClick = this.handleTreeItemDblClick.bind(this); + this.handlePlot = this.handlePlot.bind(this); + this.handleDelete = this.handleDelete.bind(this); + + // 确保 FloatingChartWindow 组件已注册 + if (!customElements.get('floating-chart-window')) { + const script = document.createElement('script'); + script.src = './components/FloatingChartWindow.js'; + document.head.appendChild(script); + } } connectedCallback() { this.isActive = true; // 设置初始状态为激活 this.loadInterfaces(); - // 延迟多次尝试,直到拿到有效宽度 - 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); - } - }; - 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; - } - + // 等待组件完全加载后初始化 setTimeout(() => { this.initializeComponent(); @@ -110,8 +72,6 @@ class DataMonitor extends HTMLElement { if (statusText) { statusText.textContent = '未监控'; } - - this.renderTable(); } catch (error) { console.error('初始化组件失败:', error); const statusIndicator = this.shadowRoot.getElementById('statusIndicator'); @@ -220,15 +180,40 @@ class DataMonitor extends HTMLElement { 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, - isMonitoring: false, - color: this.getRandomColor() + monitorData: '', + isMonitoring: false }); - this.renderTable(); + + // 创建新行 + const tbody = this.shadowRoot.querySelector('.data-table tbody'); + const tr = document.createElement('tr'); + tr.innerHTML = ` + ${item.InterfaceName} + ${item.ModelStructName} + - + - + +
+ + + + +
+ + `; + + // 添加删除按钮事件 + const deleteButton = tr.querySelector('.action-button.delete'); + deleteButton.addEventListener('click', () => { + this.handleDelete(item.InterfaceName, item.ModelStructName); + }); + + tbody.appendChild(tr); } } @@ -277,6 +262,57 @@ class DataMonitor extends HTMLElement { return size; } + /** + * @description 格式化监控数据 + * @param {string} data - 原始数据 + * @returns {string} 格式化后的数据 + */ + formatMonitorData(data) { + if (!data) return '-'; + try { + // 尝试解析JSON数据 + const parsedData = JSON.parse(data); + // 如果是对象,转换为格式化的字符串 + if (typeof parsedData === 'object') { + return JSON.stringify(parsedData, null, 2); + } + return data; + } catch (e) { + // 如果不是JSON,直接返回原始数据 + return data; + } + } + + /** + * @description 更新表格数据 + * @param {Object} newData - 新的监控数据 + */ + updateTableData(newData) { + let hasChanges = false; + + // 更新内存中的数据 + this.tableData.forEach(row => { + const newValue = newData[row.InterfaceName]; + if (newValue !== undefined && newValue !== row.monitorData) { + row.monitorData = newValue; + hasChanges = true; + } + }); + + if (hasChanges) { + // 只更新数据列的内容 + const rows = this.shadowRoot.querySelectorAll('.data-table tbody tr'); + rows.forEach((row, rowIndex) => { + const dataCell = row.querySelector('td:nth-child(3)'); + if (dataCell) { + const formattedData = this.formatMonitorData(this.tableData[rowIndex].monitorData); + dataCell.textContent = formattedData; + dataCell.title = formattedData; + } + }); + } + } + /** * @description 启动数据更新定时器 */ @@ -311,7 +347,12 @@ class DataMonitor extends HTMLElement { row.isMonitoring = false; row.monitorData = ''; }); - this.renderTable(); + // 只更新数据单元格 + const dataCells = this.shadowRoot.querySelectorAll('.data-cell'); + dataCells.forEach(cell => { + cell.textContent = '-'; + cell.title = '-'; + }); return; // 未初始化时等待下一次循环 } @@ -344,7 +385,6 @@ class DataMonitor extends HTMLElement { // 获取监控数据 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}`); @@ -364,33 +404,18 @@ class DataMonitor extends HTMLElement { } // 更新表格数据 - this.tableData.forEach(row => { - if (row.ModelStructName === structName && responseData.data[row.InterfaceName]) { - row.monitorData = responseData.data[row.InterfaceName]; - } - }); + this.updateTableData(responseData.data); } 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'); @@ -399,14 +424,16 @@ class DataMonitor extends HTMLElement { statusText.textContent = '监控错误'; } - // 清空所有监控数据 this.tableData.forEach(row => { row.isMonitoring = false; row.monitorData = ''; }); - - this.renderTable(); - alert(`数据监控发生错误: ${error.message}`); + // 只更新数据单元格 + const dataCells = this.shadowRoot.querySelectorAll('.data-cell'); + dataCells.forEach(cell => { + cell.textContent = '-'; + cell.title = '-'; + }); } }, 1000); // 每秒更新一次 } @@ -442,103 +469,6 @@ class DataMonitor extends HTMLElement { } } - /** - * @description 渲染表格内容 - */ - renderTable() { - const tableBody = this.shadowRoot.querySelector('#monitor-table-body'); - if (tableBody) { - tableBody.innerHTML = this.tableData.map((row, idx) => ` - - ${row.InterfaceName} - ${row.ModelStructName} - ${row.monitorData || ''} - - - - - - - - - - - `).join(''); - } - - // 注入值输入限制 - 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(); - }; - }); - - 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; - } - 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 } - } - } - }); - } - /** * @description 获取随机颜色 * @returns {string} 颜色字符串 @@ -553,44 +483,194 @@ class DataMonitor extends HTMLElement { } /** - * @description 更新图表,支持多条曲线 + * @description 处理绘图按钮点击事件 + * @param {string} interfaceName - 接口名称 + * @param {string} modelStructName - 结构体名称 */ - updateChart() { - if (!this.chart) return; - if (!this.activeChartIndexes || this.activeChartIndexes.length === 0) { - this.chart.data.labels = []; - this.chart.data.datasets = []; - this.chart.update('none'); + async handlePlot(interfaceName, modelStructName) { + + // 检查是否已经存在该接口的图表窗口 + const windowId = `${interfaceName}_${modelStructName}`; + if (this.chartWindows.has(windowId)) { + // 如果窗口已存在,将其置顶 + const window = this.chartWindows.get(windowId); + window.setAttribute('z-index', Date.now()); return; } - // 多条曲线 - 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 + + if (typeof Chart === 'undefined') { + console.log('Chart.js 未加载...'); + } + + try { + // 创建图表数据 + const chartData = { + labels: [], + datasets: [{ + label: interfaceName, + data: [], + borderColor: this.getRandomColor(), + fill: false + }] }; - }); - this.chart.update('none'); + + // 创建图表配置 + const chartOptions = { + responsive: true, + maintainAspectRatio: false, + animation: false, + elements: { + point: { + radius: 0 + }, + line: { + tension: 0 + } + }, + scales: { + y: { + beginAtZero: false, + ticks: { + callback: function(value) { + // 如果数值的绝对值大于1000或小于0.1,使用科学计数法 + if (Math.abs(value) > 1000 || (Math.abs(value) < 0.1 && value !== 0)) { + return value.toExponential(1); + } + // 否则使用普通数字,保留一位小数 + return value.toFixed(1); + }, + maxTicksLimit: 8 // 限制刻度数量 + } + }, + x: { + display: true, + ticks: { + callback: function(value) { + // 如果数值大于1000,使用科学计数法 + if (value > 1000) { + return value.toExponential(1); + } + return value; + } + } + } + }, + plugins: { + legend: { + display: true, + position: 'top' + }, + tooltip: { + enabled: true, + mode: 'index', + intersect: false, + callbacks: { + label: function(context) { + const value = context.raw; + // 如果数值的绝对值大于1000或小于0.1,使用科学计数法 + if (Math.abs(value) > 1000 || (Math.abs(value) < 0.1 && value !== 0)) { + return `${context.dataset.label}: ${value.toExponential(1)}`; + } + // 否则使用普通数字,保留一位小数 + return `${context.dataset.label}: ${value.toFixed(1)}`; + } + } + } + } + }; + + // 创建浮动窗口组件 + const floatingWindow = document.createElement('floating-chart-window'); + if (!floatingWindow) { + throw new Error('创建浮动窗口组件失败'); + } + + // 添加数据点计数器 + floatingWindow.dataPointIndex = 0; + + // 先添加到页面 + document.body.appendChild(floatingWindow); + this.chartWindows.set(windowId, floatingWindow); + + // 再设置属性 + floatingWindow.setAttribute('title', interfaceName); + floatingWindow.setAttribute('initial-position', JSON.stringify({ x: 400, y: 200 })); // 调整初始位置 + floatingWindow.setAttribute('initial-size', JSON.stringify({ width: 400, height: 300 })); + floatingWindow.setAttribute('chart-data', JSON.stringify(chartData)); + floatingWindow.setAttribute('chart-options', JSON.stringify(chartOptions)); + + // 更新图表数据 + const updateChartData = () => { + const row = this.tableData.find(r => + r.InterfaceName === interfaceName && + r.ModelStructName === modelStructName + ); + + if (row && row.monitorData) { + try { + const data = JSON.parse(row.monitorData); + let value; + // 处理数值类型 + if (typeof data === 'number') { + value = data; + } else if (typeof data === 'object') { + value = JSON.stringify(data); + } else { + // 尝试将字符串转换为数字 + const numValue = parseFloat(data); + value = isNaN(numValue) ? data : numValue; + } + + // 使用独立的数据点计数器 + chartData.labels.push(floatingWindow.dataPointIndex.toString()); + chartData.datasets[0].data.push(value); + floatingWindow.dataPointIndex++; // 增加计数器 + + // 保持最近100个数据点 + if (chartData.labels.length > 100) { + chartData.labels.shift(); + chartData.datasets[0].data.shift(); + } + + // 如果是第一个数据点,设置y轴范围 + if (chartData.datasets[0].data.length === 1 && typeof value === 'number') { + // 计算合适的y轴范围 + const range = Math.abs(value) * 0.2; // 使用20%的范围 + chartOptions.scales.y.min = value - range; + chartOptions.scales.y.max = value + range; + // 更新图表配置 + floatingWindow.setAttribute('chart-options', JSON.stringify(chartOptions)); + } + + // 更新图表数据 + floatingWindow.setAttribute('chart-data', JSON.stringify(chartData)); + } catch (e) { + console.error('解析数据失败:', e); + } + } + }; + + // 将更新函数添加到数据更新定时器中 + const originalUpdateTableData = this.updateTableData; + this.updateTableData = (newData) => { + originalUpdateTableData.call(this, newData); + updateChartData(); + }; + + // 添加关闭事件处理 + floatingWindow.addEventListener('close', () => { + // 恢复原始的 updateTableData 方法 + this.updateTableData = originalUpdateTableData; + // 从 Map 中移除窗口引用 + this.chartWindows.delete(windowId); + }); + } catch (error) { + console.error('创建图表窗口失败:', error); + alert('创建图表窗口失败,请查看控制台了解详情'); + } } render() { - // 计算所有列宽之和 - const 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] || []; @@ -659,29 +739,6 @@ class DataMonitor extends HTMLElement { background: #d9d9d9; } - .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; - } - .monitor-container { background-color: white; border-radius: 8px; @@ -764,213 +821,179 @@ class DataMonitor extends HTMLElement { overflow: hidden; } - .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; - display: flex; - flex-direction: column; - } - .table-scroll-x { - width: 100%; - height: 100%; - overflow-x: auto; - overflow-y: hidden; - display: flex; - flex-direction: column; - } - .table-header-fixed { - overflow: hidden; - width: fit-content; - } - .table-header-fixed .monitor-table { - width: max-content; - min-width: 100%; - } - .table-body-scroll { + .table-container { flex: 1; - overflow-y: auto; + overflow: auto; + margin-top: 16px; width: 100%; - overflow-x: hidden; - overflow-x: auto; - max-height: calc(100% - 1px); + position: relative; } - .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; + + .data-table { + width: 100%; + border-collapse: collapse; + background: white; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0,0,0,0.06); table-layout: fixed; - min-width: unset; } - .monitor-table th, .monitor-table td { - padding: 8px; - border-bottom: 1px solid #e0e0e0; + + .data-table th, + .data-table td { + padding: 12px 16px; text-align: left; - background: #fff; + border-bottom: 1px solid #f0f0f0; + border-right: 1px solid #f0f0f0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; position: relative; - max-width: 0; } - .cell-text { - display: inline-block; - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - vertical-align: middle; + + .data-table th:last-child, + .data-table td:last-child { + border-right: none; + width: 300px !important; /* 固定操作列宽度 */ } - .monitor-table input, - .monitor-table button { - min-width: 0; - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + + .data-table th:last-child .resizer { + display: none; /* 隐藏操作列的调整器 */ } - .monitor-table th { - background: #f5f5f5; - position: sticky; - top: 0; - z-index: 2; - position: relative; + + .data-table th { + background: #fafafa; + font-weight: 500; + color: #262626; + user-select: none; } - /* 竖线:表头和内容区 */ - .monitor-table td:not(:last-child)::after { - content: ''; + + .data-table tr:hover { + background: #fafafa; + } + + .resizer { position: absolute; right: 0; - top: 20%; - width: 1px; - height: 60%; - background: #e0e0e0; - z-index: 1; + top: 0; + bottom: 0; + width: 5px; + cursor: col-resize; + background: transparent; } - .monitor-table th.th-resize-active::after { + + .resizer:hover, + .resizer.active { background: #1890ff; } - .divider { - height: 12px; + .action-buttons { + display: flex; + gap: 8px; + } + + .action-button { + padding: 4px 8px; + border: 1px solid #d9d9d9; + border-radius: 4px; + background: white; + cursor: pointer; + font-size: 12px; + color: #262626; + transition: all 0.3s; + } + + .action-button:hover { + color: #1890ff; + border-color: #1890ff; + } + + .action-button.delete { + color: #ff4d4f; + } + + .action-button.delete:hover { + border-color: #ff4d4f; + } + + .data-cell { + font-family: monospace; + white-space: pre-wrap; + word-break: break-all; + max-height: 100px; + overflow-y: auto; + } + + .floating-chart-window { + position: fixed; + width: 400px; + height: 300px; + background: white; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + display: flex; + flex-direction: column; + overflow: hidden; + left: 100px; + top: 100px; + } + + .window-header { + background: #f5f5f5; + padding: 8px 12px; + display: flex; + justify-content: space-between; + align-items: center; + cursor: move; + user-select: none; + border-bottom: 1px solid #e8e8e8; + } + + .window-title { + font-size: 14px; + font-weight: 500; + color: #262626; + } + + .window-controls { + display: flex; + gap: 8px; + } + + .window-control-button { + width: 24px; + height: 24px; + border: none; background: transparent; - cursor: row-resize; - width: 100%; - position: relative; - z-index: 2; - transition: background 0.2s; + cursor: pointer; display: flex; align-items: center; justify-content: center; - } - .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; - } - .divider:hover .divider-bar, - .divider.active .divider-bar { - background: #1890ff; - box-shadow: 0 2px 8px rgba(24,144,255,0.15); + font-size: 16px; + color: #666; + border-radius: 4px; + transition: all 0.3s; } - .chart-section { - background: #fff; - border-radius: 0 0 8px 8px; - box-shadow: 0 2px 8px rgba(0,0,0,0.06); + .window-control-button:hover { + background: rgba(0, 0, 0, 0.05); + color: #262626; + } + + .window-control-button.close-button:hover { + background: #ff4d4f; + color: white; + } + + .window-content { + flex: 1; padding: 12px; - flex: 1; - display: flex; - flex-direction: column; - min-height: 120px; - min-width: 0; - overflow: hidden; - } - .chart-title { - font-size: 15px; - font-weight: bold; - margin-bottom: 8px; - } - .chart-container { - flex: 1; - min-height: 120px; position: relative; } - #monitor-chart { + + .window-content canvas { width: 100% !important; height: 100% !important; - display: block; - } - - .action-btn { - margin: 0 2px; - padding: 3px 8px; - border: none; - border-radius: 4px; - background: #f0f0f0; - color: #333; - font-size: 13px; - cursor: pointer; - transition: background 0.2s; - } - .action-btn:hover { - background: #e6f7ff; - } - .chart-btn.drawing { - background: #1890ff; - color: #fff; - } - - .th-resize { - position: absolute; - right: 0; - top: 0; - width: 12px; - height: 100%; - cursor: col-resize; - z-index: 10; - user-select: none; - display: flex; - 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; - } - .monitor-btn.monitoring { - background: #ff4d4f; - color: #fff; - } - .action-btn:disabled { - opacity: 0.5; - cursor: not-allowed; }
@@ -1008,48 +1031,35 @@ class DataMonitor extends HTMLElement {
-
-
- - - - - - - - - - - - - - - +
+
接口名称
结构体名
数据
注入值
操作
+ + + + + + + + + + + ${this.tableData.map(row => ` + + + + + - -
接口名
结构体名
数据
注入值
操作
${row.InterfaceName}${row.ModelStructName}${this.formatMonitorData(row.monitorData)}${row.InjectValue || '-'} +
+ + + + +
+
-
-
- - - - - - - - - - - -
-
-
-
-
-
数据监控图表
-
- -
+ `).join('')} + +
@@ -1070,75 +1080,114 @@ class DataMonitor extends HTMLElement { }; }); - // 渲染表格内容 - this.renderTable(); + // 添加列宽调整功能 + const table = this.shadowRoot.querySelector('.data-table'); + const resizers = table.querySelectorAll('.resizer'); + + resizers.forEach(resizer => { + let startX, startWidth; - // 初始化图表 - 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) => { + resizer.addEventListener('mousedown', (e) => { e.preventDefault(); - this.dragColIndex = parseInt(handle.getAttribute('data-col')); - this.dragStartX = e.clientX; - this.dragStartWidth = this.colWidths[this.dragColIndex]; + const th = resizer.parentElement; + startX = e.pageX; + startWidth = th.offsetWidth; + + const mouseMoveHandler = (e) => { + const width = startWidth + (e.pageX - startX); + if (width > 50) { + th.style.width = width + 'px'; + } + }; + + const mouseUpHandler = () => { + document.removeEventListener('mousemove', mouseMoveHandler); + document.removeEventListener('mouseup', mouseUpHandler); + document.body.style.cursor = 'default'; + resizer.classList.remove('active'); + }; + + document.addEventListener('mousemove', mouseMoveHandler); + document.addEventListener('mouseup', mouseUpHandler); document.body.style.cursor = 'col-resize'; - 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'); - }; + resizer.classList.add('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(); + + // 添加删除按钮的事件委托 + 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); } - }; - 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 = ''; + }); + + // 添加绘图按钮的事件委托 + 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); } - }; + }); + } + + /** + * @description 处理删除按钮点击事件 + * @param {string} interfaceName - 接口名称 + * @param {string} modelStructName - 结构体名称 + */ + async handleDelete(interfaceName, modelStructName) { + try { + // 检查是否还有其他相同结构体的接口 + const sameStructInterfaces = this.tableData.filter(row => + row.ModelStructName === modelStructName && + !(row.InterfaceName === interfaceName && row.ModelStructName === modelStructName) + ); + + // 如果当前正在监控中,并且这是该结构体的最后一个接口,停止对该结构体的监控 + const statusIndicator = this.shadowRoot.getElementById('statusIndicator'); + const isMonitoring = statusIndicator && statusIndicator.classList.contains('active'); + + if (isMonitoring && sameStructInterfaces.length === 0) { + await fetch('/api/data-monitor/stop', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ structName: modelStructName }) + }); + } + + // 从表格数据中移除 + this.tableData = this.tableData.filter(row => + !(row.InterfaceName === interfaceName && row.ModelStructName === modelStructName) + ); + + // 找到并移除对应的表格行 + const rows = this.shadowRoot.querySelectorAll('.data-table tbody tr'); + rows.forEach(row => { + if (row.cells[0].textContent === interfaceName && row.cells[1].textContent === modelStructName) { + row.remove(); + } + }); + } catch (error) { + console.error('删除接口时发生错误:', error); + } } disconnectedCallback() { this.isActive = false; + // 清理所有图表窗口 + this.chartWindows.forEach(window => { + window.remove(); + }); + this.chartWindows.clear(); // 组件销毁时清理定时器 this.stopDataUpdateTimer(); } } -customElements.define('data-monitor', DataMonitor); \ No newline at end of file +customElements.define('data-monitor', DataMonitor); +