- 添加 appbar 导航栏,支持 Chart/Queues 视图切换 - appbar 切换使用 history API,支持浏览器前进/后退 - 图表视图占满整个可视区域 - queue-modal 共享 appbar 样式 - 修复 queue tab count 字段名大小写问题 - tooltip 跟随鼠标显示在右下方,移除箭头 - 图表 canvas 鼠标样式改为准星 - pause/resume 队列后刷新列表 - example 添加 flag 配置参数
281 lines
8.4 KiB
JavaScript
281 lines
8.4 KiB
JavaScript
import { LitElement, html, css } from 'lit';
|
||
import { Chart, registerables, Tooltip } from 'chart.js';
|
||
|
||
Chart.register(...registerables);
|
||
|
||
// 自定义 tooltip positioner:显示在鼠标右下方
|
||
Tooltip.positioners.cursor = function (elements, eventPosition, tooltip) {
|
||
return {
|
||
x: eventPosition.x + 20,
|
||
y: eventPosition.y + 15,
|
||
xAlign: 'left',
|
||
yAlign: 'top'
|
||
};
|
||
};
|
||
|
||
// 十字准星 + 拖拽选择插件
|
||
const crosshairPlugin = {
|
||
id: 'crosshair',
|
||
afterEvent(chart, args) {
|
||
const event = args.event;
|
||
if (event.type === 'mousemove') {
|
||
chart.crosshair = { x: event.x, y: event.y };
|
||
} else if (event.type === 'mouseout') {
|
||
chart.crosshair = null;
|
||
}
|
||
},
|
||
afterDraw(chart) {
|
||
const ctx = chart.ctx;
|
||
const topY = chart.scales.y.top;
|
||
const bottomY = chart.scales.y.bottom;
|
||
const leftX = chart.scales.x.left;
|
||
const rightX = chart.scales.x.right;
|
||
|
||
// 绘制选择区域
|
||
if (chart.dragSelect && chart.dragSelect.startX !== null) {
|
||
const { startX, currentX } = chart.dragSelect;
|
||
const x1 = Math.max(leftX, Math.min(startX, currentX));
|
||
const x2 = Math.min(rightX, Math.max(startX, currentX));
|
||
|
||
ctx.save();
|
||
ctx.fillStyle = 'rgba(255, 193, 7, 0.2)';
|
||
ctx.fillRect(x1, topY, x2 - x1, bottomY - topY);
|
||
ctx.restore();
|
||
}
|
||
|
||
// 绘制十字准星
|
||
if (!chart.crosshair) return;
|
||
const { x, y } = chart.crosshair;
|
||
|
||
// 检查鼠标是否在图表区域内
|
||
if (x < leftX || x > rightX || y < topY || y > bottomY) return;
|
||
|
||
ctx.save();
|
||
ctx.setLineDash([4, 4]);
|
||
ctx.lineWidth = 1;
|
||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';
|
||
|
||
// 垂直线
|
||
ctx.beginPath();
|
||
ctx.moveTo(x, topY);
|
||
ctx.lineTo(x, bottomY);
|
||
ctx.stroke();
|
||
|
||
// 水平线跟随鼠标
|
||
ctx.beginPath();
|
||
ctx.moveTo(leftX, y);
|
||
ctx.lineTo(rightX, y);
|
||
ctx.stroke();
|
||
|
||
ctx.restore();
|
||
}
|
||
};
|
||
Chart.register(crosshairPlugin);
|
||
|
||
class TasksChart extends LitElement {
|
||
static properties = {
|
||
data: { type: Object },
|
||
timestamps: { type: Array }
|
||
};
|
||
|
||
static styles = css`
|
||
:host {
|
||
display: block;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
canvas {
|
||
width: 100% !important;
|
||
height: 100% !important;
|
||
cursor: crosshair;
|
||
}
|
||
`;
|
||
|
||
constructor() {
|
||
super();
|
||
this.data = { labels: [], datasets: {} };
|
||
this.timestamps = [];
|
||
this.chart = null;
|
||
this.isDragging = false;
|
||
}
|
||
|
||
firstUpdated() {
|
||
this.initChart();
|
||
this.setupDragSelect();
|
||
}
|
||
|
||
setupDragSelect() {
|
||
const canvas = this.shadowRoot.querySelector('canvas');
|
||
if (!canvas) return;
|
||
|
||
canvas.addEventListener('mousedown', (e) => {
|
||
if (!this.chart) return;
|
||
const rect = canvas.getBoundingClientRect();
|
||
const x = e.clientX - rect.left;
|
||
const leftX = this.chart.scales.x.left;
|
||
const rightX = this.chart.scales.x.right;
|
||
|
||
if (x >= leftX && x <= rightX) {
|
||
this.isDragging = true;
|
||
this.chart.dragSelect = { startX: x, currentX: x };
|
||
this.chart.update('none');
|
||
}
|
||
});
|
||
|
||
canvas.addEventListener('mousemove', (e) => {
|
||
if (!this.isDragging || !this.chart) return;
|
||
const rect = canvas.getBoundingClientRect();
|
||
const x = e.clientX - rect.left;
|
||
this.chart.dragSelect.currentX = x;
|
||
this.chart.update('none');
|
||
});
|
||
|
||
canvas.addEventListener('mouseup', (e) => {
|
||
if (!this.isDragging || !this.chart) return;
|
||
this.isDragging = false;
|
||
|
||
const { startX, currentX } = this.chart.dragSelect;
|
||
const leftX = this.chart.scales.x.left;
|
||
const rightX = this.chart.scales.x.right;
|
||
|
||
// 计算选择的时间范围
|
||
const x1 = Math.max(leftX, Math.min(startX, currentX));
|
||
const x2 = Math.min(rightX, Math.max(startX, currentX));
|
||
|
||
// 最小选择宽度检查
|
||
if (Math.abs(x2 - x1) > 10) {
|
||
const startIndex = Math.round((x1 - leftX) / (rightX - leftX) * (this.timestamps.length - 1));
|
||
const endIndex = Math.round((x2 - leftX) / (rightX - leftX) * (this.timestamps.length - 1));
|
||
|
||
if (startIndex >= 0 && endIndex < this.timestamps.length) {
|
||
const startTime = this.timestamps[startIndex];
|
||
const endTime = this.timestamps[endIndex];
|
||
|
||
this.dispatchEvent(new CustomEvent('time-range-select', {
|
||
detail: { start: startTime, end: endTime },
|
||
bubbles: true,
|
||
composed: true
|
||
}));
|
||
}
|
||
}
|
||
|
||
this.chart.dragSelect = null;
|
||
this.chart.update('none');
|
||
});
|
||
|
||
canvas.addEventListener('mouseleave', () => {
|
||
if (this.isDragging && this.chart) {
|
||
this.isDragging = false;
|
||
this.chart.dragSelect = null;
|
||
this.chart.update('none');
|
||
}
|
||
});
|
||
}
|
||
|
||
initChart() {
|
||
const canvas = this.shadowRoot.querySelector('canvas');
|
||
if (!canvas) return;
|
||
|
||
const ctx = canvas.getContext('2d');
|
||
|
||
this.chart = new Chart(ctx, {
|
||
type: 'line',
|
||
data: {
|
||
labels: [],
|
||
datasets: [
|
||
{ label: 'Active', data: [], borderColor: '#42a5f5', backgroundColor: 'transparent', fill: false, hidden: true },
|
||
{ label: 'Pending', data: [], borderColor: '#5c6bc0', backgroundColor: 'transparent', fill: false },
|
||
{ label: 'Scheduled', data: [], borderColor: '#ffa726', backgroundColor: 'transparent', fill: false, hidden: true },
|
||
{ label: 'Retry', data: [], borderColor: '#ef5350', backgroundColor: 'transparent', fill: false, hidden: true },
|
||
{ label: 'Archived', data: [], borderColor: '#ab47bc', backgroundColor: 'transparent', fill: false, hidden: true },
|
||
{ label: 'Completed', data: [], borderColor: '#66bb6a', backgroundColor: 'transparent', fill: false, hidden: true },
|
||
{ label: 'Succeeded', data: [], borderColor: '#81c784', backgroundColor: 'transparent', fill: false, borderDash: [5, 5], hidden: true },
|
||
{ label: 'Failed', data: [], borderColor: '#e57373', backgroundColor: 'transparent', fill: false, borderDash: [5, 5], hidden: true }
|
||
]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
animation: { duration: 0 },
|
||
clip: false,
|
||
interaction: { mode: 'index', intersect: false },
|
||
hover: { mode: 'index', intersect: false },
|
||
plugins: {
|
||
legend: {
|
||
position: 'bottom',
|
||
labels: {
|
||
color: '#e0e0e0',
|
||
padding: 15,
|
||
usePointStyle: true,
|
||
pointStyle: 'circle'
|
||
}
|
||
},
|
||
tooltip: {
|
||
enabled: true,
|
||
backgroundColor: 'rgba(30, 30, 30, 0.9)',
|
||
titleColor: '#e0e0e0',
|
||
bodyColor: '#e0e0e0',
|
||
borderColor: '#616161',
|
||
borderWidth: 1,
|
||
padding: 10,
|
||
displayColors: true,
|
||
position: 'cursor',
|
||
caretSize: 0
|
||
}
|
||
},
|
||
scales: {
|
||
x: {
|
||
grid: { color: '#616161' },
|
||
ticks: { color: '#9e9e9e', maxTicksLimit: 10 }
|
||
},
|
||
y: {
|
||
grid: { color: '#616161' },
|
||
ticks: { color: '#9e9e9e' },
|
||
beginAtZero: true
|
||
}
|
||
},
|
||
elements: {
|
||
point: {
|
||
radius: 0,
|
||
hoverRadius: 5,
|
||
hitRadius: 10,
|
||
hoverBackgroundColor: (ctx) => ctx.dataset.borderColor,
|
||
hoverBorderColor: (ctx) => ctx.dataset.borderColor,
|
||
hoverBorderWidth: 0
|
||
},
|
||
line: { tension: 0.3, borderWidth: 1.5, spanGaps: false }
|
||
}
|
||
}
|
||
});
|
||
|
||
this.updateChart();
|
||
}
|
||
|
||
updated(changedProperties) {
|
||
if (changedProperties.has('data') && this.chart) {
|
||
this.updateChart();
|
||
}
|
||
}
|
||
|
||
updateChart() {
|
||
if (!this.chart || !this.data) return;
|
||
|
||
this.chart.data.labels = this.data.labels || [];
|
||
this.chart.data.datasets[0].data = this.data.datasets?.active || [];
|
||
this.chart.data.datasets[1].data = this.data.datasets?.pending || [];
|
||
this.chart.data.datasets[2].data = this.data.datasets?.scheduled || [];
|
||
this.chart.data.datasets[3].data = this.data.datasets?.retry || [];
|
||
this.chart.data.datasets[4].data = this.data.datasets?.archived || [];
|
||
this.chart.data.datasets[5].data = this.data.datasets?.completed || [];
|
||
this.chart.data.datasets[6].data = this.data.datasets?.succeeded || [];
|
||
this.chart.data.datasets[7].data = this.data.datasets?.failed || [];
|
||
this.chart.update('none');
|
||
}
|
||
|
||
render() {
|
||
return html`<canvas></canvas>`;
|
||
}
|
||
}
|
||
|
||
customElements.define('tasks-chart', TasksChart);
|