数据监控页面重做

This commit is contained in:
jinchao 2025-06-10 15:21:07 +08:00
parent d53274288a
commit 1452ac2dad
4 changed files with 1231 additions and 506 deletions

Binary file not shown.

View File

@ -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;
}

View File

@ -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 = `
<div class="floating-window">
<div class="window-header">
<span class="window-title">${this.getAttribute('title') || '图表窗口'}</span>
<div class="window-controls">
<button class="window-control-button minimize-button"></button>
<button class="window-control-button close-button">×</button>
</div>
</div>
<div class="window-content">
<div class="chart-container">
<canvas></canvas>
</div>
</div>
<div class="resize-handle"></div>
</div>
`;
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);

File diff suppressed because it is too large Load Diff