Files
taskq/ui/components/time-range-picker.js
hupeh 1f9f1cab53 feat: 添加监控仪表盘
- 新增 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 添加延迟/定点任务示例
2025-12-09 19:58:18 +08:00

166 lines
4.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { LitElement, html, css } from 'lit';
class TimeRangePicker extends LitElement {
static properties = {
duration: { type: String },
endTime: { type: Number },
isLiveMode: { type: Boolean }
};
static durations = ['5m', '15m', '30m', '1h', '3h', '6h', '12h', '1d', '3d', '7d'];
static styles = css`
:host {
display: flex;
gap: 10px;
align-items: center;
}
.time-control {
display: flex;
align-items: center;
background: #424242;
border: 1px solid #616161;
border-radius: 4px;
overflow: hidden;
}
button {
background: transparent;
border: none;
color: #9e9e9e;
padding: 6px 10px;
cursor: pointer;
font-size: 1em;
}
button:hover {
background: #515151;
color: #e0e0e0;
}
.value {
padding: 6px 12px;
color: #e0e0e0;
font-size: 0.85em;
min-width: 40px;
text-align: center;
}
.end-value {
cursor: pointer;
}
.end-value:hover {
background: #515151;
}
.reset-btn:hover {
color: #ef5350;
}
`;
constructor() {
super();
this.duration = '1h';
this.endTime = null;
this.isLiveMode = true;
}
get durationIndex() {
return TimeRangePicker.durations.indexOf(this.duration);
}
parseDuration(dur) {
const match = dur.match(/^(\d+)([mhd])$/);
if (!match) return 3600;
const value = parseInt(match[1]);
const unit = match[2];
switch (unit) {
case 'm': return value * 60;
case 'h': return value * 3600;
case 'd': return value * 86400;
default: return 3600;
}
}
adjustDuration(delta) {
const durations = TimeRangePicker.durations;
const newIndex = Math.max(0, Math.min(durations.length - 1, this.durationIndex + delta));
this.duration = durations[newIndex];
this.emitChange();
}
adjustEndTime(delta) {
const durationSecs = this.parseDuration(this.duration);
const step = durationSecs / 2;
let newEndTime = this.endTime;
if (newEndTime === null) {
newEndTime = Math.floor(Date.now() / 1000);
}
newEndTime += delta * step;
const now = Math.floor(Date.now() / 1000);
if (newEndTime >= now) {
this.resetToNow();
return;
}
this.endTime = newEndTime;
this.isLiveMode = false;
this.emitChange();
}
resetToNow() {
this.endTime = null;
this.isLiveMode = true;
this.emitChange();
}
emitChange() {
this.dispatchEvent(new CustomEvent('change', {
detail: {
duration: this.duration,
endTime: this.endTime,
isLiveMode: this.isLiveMode
}
}));
}
formatEndTime() {
if (this.endTime === null) {
return 'now';
}
const date = new Date(this.endTime * 1000);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).replace(/\//g, '-');
}
render() {
return html`
<div class="time-control">
<button @click=${() => this.adjustDuration(-1)}></button>
<span class="value">${this.duration}</span>
<button @click=${() => this.adjustDuration(1)}>+</button>
</div>
<div class="time-control">
<button @click=${() => this.adjustEndTime(-1)}></button>
<span class="value end-value" @click=${this.resetToNow}>
${this.formatEndTime()}
</span>
<button @click=${() => this.adjustEndTime(1)}></button>
<button class="reset-btn" @click=${this.resetToNow} title="Reset to now">×</button>
</div>
`;
}
}
customElements.define('time-range-picker', TimeRangePicker);