数据监控页面重做
This commit is contained in:
parent
d53274288a
commit
1452ac2dad
Binary file not shown.
106
XNSimHtml/components/FloatingChartWindow.css
Normal file
106
XNSimHtml/components/FloatingChartWindow.css
Normal 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;
|
||||
}
|
570
XNSimHtml/components/FloatingChartWindow.js
Normal file
570
XNSimHtml/components/FloatingChartWindow.js
Normal 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
Loading…
x
Reference in New Issue
Block a user