// 确保 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; } .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 || {}) } } } }; // 确保数据集配置正确 if (chartData.datasets && chartData.datasets.length > 0) { chartData.datasets = chartData.datasets.map(dataset => ({ ...dataset, borderWidth: 2, pointRadius: 0, tension: 0 })); } this.chartInstance = new Chart(ctx, { type: 'line', data: chartData, options: finalOptions }); } addEventListeners() { const header = this.shadowRoot.querySelector('.window-header'); const closeButton = this.shadowRoot.querySelector('.close-button'); const resizeHandle = this.shadowRoot.querySelector('.resize-handle'); header.addEventListener('mousedown', this.handleMouseDown); 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.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) { 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) { 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)}`; }; // 计算所有数据点的范围 let min = Infinity; let max = -Infinity; data.datasets.forEach(dataset => { if (dataset.data && dataset.data.length > 0) { const datasetMin = Math.min(...dataset.data); const datasetMax = Math.max(...dataset.data); min = Math.min(min, datasetMin); max = Math.max(max, datasetMax); } }); if (min !== Infinity && max !== -Infinity) { const range = max - min; // 如果所有数据都是0,使用固定范围 if (min === 0 && max === 0) { options.scales.y.min = -1; options.scales.y.max = 1; } // 如果范围很小,使用固定比例 else if (range < 1) { options.scales.y.min = min - 0.5; options.scales.y.max = max + 0.5; } else { // 如果范围较大,使用百分比 const margin = range * 0.2; // 使用20%的边距 options.scales.y.min = min - margin; options.scales.y.max = max + margin; } } // 更新图表 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; } } // 添加新方法:处理数据更新 handleDataUpdate(values, interfaceName) { if (!this.chartInstance) return; const chartData = this.chartInstance.data; // 如果是第一次收到数据,创建数据集 if (chartData.datasets.length === 0) { if (values.length > 1) { // 创建多个数据集 chartData.datasets = values.map((_, index) => ({ label: `${interfaceName} (${index + 1})`, data: [], borderColor: this.getRandomColor(), fill: false, borderWidth: 2, pointRadius: 0, tension: 0 })); } else { // 创建单个数据集 chartData.datasets = [{ label: interfaceName, data: [], borderColor: this.getRandomColor(), fill: false, borderWidth: 2, pointRadius: 0, tension: 0 }]; } } // 添加新的数据点 chartData.labels.push(this.dataPointIndex.toString()); values.forEach((value, index) => { if (index < chartData.datasets.length) { chartData.datasets[index].data.push(value); } }); this.dataPointIndex++; // 保持最近100个数据点 if (chartData.labels.length > 100) { chartData.labels.shift(); chartData.datasets.forEach(dataset => { dataset.data.shift(); }); } // 更新图表 this.updateChartData(chartData); } // 添加新方法:获取随机颜色 getRandomColor() { const letters = '0123456789ABCDEF'; let color = '#'; for (let i = 0; i < 6; i++) { color += letters[Math.floor(Math.random() * 16)]; } return color; } } customElements.define('floating-chart-window', FloatingChartWindow);