- 新增 Lit.js 组件化 UI (ui/ 目录) - tasks-chart: 带十字准星和拖拽选择的图表 - queue-table: 队列列表,支持暂停/恢复 - queue-modal: 队列详情弹窗,支持任务重试 - time-range-picker: Prometheus 风格时间选择器 - help-tooltip: 可复用的提示组件 - HTTPHandler 功能 - SSE 实时推送 (stats + queues) - 队列暂停/恢复 API - 任务重试 API - 时间范围查询 API - Inspector 改进 - Prometheus 风格单表存储 - 集成到 Start/Stop 生命周期 - 新增 PauseQueue/UnpauseQueue/RunTask 方法 - 代码重构 - Start 函数拆分为小函数 - 优雅关闭流程优化 - 其他 - 忽略 SQLite 数据库文件 - example 添加延迟/定点任务示例
268 lines
8.1 KiB
JavaScript
268 lines
8.1 KiB
JavaScript
import { LitElement, html, css } from 'lit';
|
|
import { Chart, registerables } from 'chart.js';
|
|
|
|
Chart.register(...registerables);
|
|
|
|
// 十字准星 + 拖拽选择插件
|
|
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;
|
|
}
|
|
`;
|
|
|
|
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
|
|
}
|
|
},
|
|
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);
|