Commit a385cade authored by Yaowentong's avatar Yaowentong

修复

parent bb703b73
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", Arial, sans-serif;
background: #eef2f7;
color: #1f2937;
}
.layout {
display: grid;
grid-template-columns: 220px minmax(0, 1fr);
min-height: 100vh;
}
.sidebar {
padding: 28px 18px;
background: #17202b;
border-right: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 10px 0 24px rgba(15, 23, 42, 0.12);
}
.nav-btn {
display: flex;
align-items: center;
width: 100%;
min-height: 46px;
margin-bottom: 10px;
border: 1px solid transparent;
border-radius: 10px;
background: rgba(255, 255, 255, 0.04);
color: #cbd5e1;
font-size: 15px;
font-weight: 700;
line-height: 20px;
text-align: left;
padding: 0 14px;
cursor: pointer;
transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease;
}
.nav-btn.active,
.nav-btn:hover {
background: #2563eb;
border-color: rgba(255, 255, 255, 0.14);
color: #fff;
}
.main {
min-width: 0;
padding: 30px;
background: #eef2f7;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.title {
color: #111827;
font-size: 28px;
font-weight: 700;
}
.desc {
margin-top: 6px;
font-size: 14px;
color: #6b7280;
}
.right {
display: flex;
align-items: center;
gap: 12px;
}
.last-time,
.section-note {
font-size: 13px;
color: #6b7280;
}
.btn,
.quick-btn {
border: none;
border-radius: 8px;
background: #2563eb;
color: #fff;
padding: 10px 18px;
cursor: pointer;
font-size: 14px;
font-weight: 700;
box-shadow: 0 10px 18px rgba(37, 99, 235, 0.18);
transition: background 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
}
.btn:hover,
.quick-btn:hover {
background: #1d4ed8;
box-shadow: 0 12px 22px rgba(37, 99, 235, 0.24);
}
.quick-btn {
background: #f1f5f9;
color: #374151;
box-shadow: none;
}
.quick-btn.active {
background: #2563eb;
color: #fff;
box-shadow: 0 10px 18px rgba(37, 99, 235, 0.18);
}
.panel {
display: none;
}
.panel.active {
display: block;
}
.summary {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.card,
.table-wrap,
.trend-panel {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 14px;
box-shadow: 0 14px 30px rgba(15, 23, 42, 0.06);
}
.card {
padding: 20px;
}
.card-name {
color: #6b7280;
font-size: 14px;
margin-bottom: 10px;
}
.card-value {
font-size: 30px;
font-weight: 700;
color: #111827;
}
.section-head {
display: flex;
justify-content: space-between;
align-items: center;
margin: 8px 0 12px;
}
.section-title {
color: #111827;
font-size: 20px;
font-weight: 700;
}
.table-wrap {
overflow: hidden;
margin-bottom: 24px;
}
.trend-panel {
position: relative;
padding: 18px;
}
.trend-loading-mask {
position: absolute;
inset: 0;
z-index: 20;
display: none;
align-items: center;
justify-content: center;
border-radius: 14px;
background: rgba(248, 250, 252, 0.78);
backdrop-filter: blur(2px);
}
.trend-panel.loading .trend-loading-mask {
display: flex;
}
.loading-box {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
border: 1px solid #dbeafe;
border-radius: 12px;
background: #fff;
color: #2563eb;
font-size: 14px;
font-weight: 800;
box-shadow: 0 14px 30px rgba(15, 23, 42, 0.12);
}
.loading-spinner {
width: 16px;
height: 16px;
border: 2px solid #bfdbfe;
border-top-color: #2563eb;
border-radius: 999px;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.filter-row {
display: flex;
flex-wrap: wrap;
align-items: end;
gap: 12px;
margin-bottom: 20px;
padding: 14px;
border-radius: 12px;
background: #f8fafc;
border: 1px solid #edf2f7;
}
.quick-range {
display: flex;
gap: 8px;
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
}
.field label {
font-size: 14px;
color: #6b7280;
}
.field input {
min-width: 220px;
border: 1px solid #d7dce3;
border-radius: 8px;
padding: 9px 10px;
background: #fff;
color: #1f2937;
font-size: 14px;
}
.field input:focus {
outline: none;
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
}
.trend-chart {
width: 100%;
min-height: 300px;
overflow-x: auto;
border-radius: 12px;
background: #f8fafc;
border: 1px solid #edf2f7;
}
.daily-grid {
display: grid;
grid-auto-flow: column;
grid-auto-columns: minmax(320px, 1fr);
gap: 14px;
min-width: 100%;
padding: 14px;
}
.day-card {
min-width: 0;
border: 1px solid #e5e7eb;
border-radius: 12px;
background: #fff;
box-shadow: 0 10px 22px rgba(15, 23, 42, 0.04);
}
.day-card-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 14px 10px;
border-bottom: 1px solid #edf2f7;
}
.day-date {
color: #111827;
font-size: 16px;
font-weight: 800;
}
.day-total {
color: #64748b;
font-size: 13px;
font-weight: 700;
}
.day-list {
display: grid;
gap: 10px;
padding: 12px 14px 14px;
}
.day-row {
display: grid;
grid-template-columns: minmax(100px, 1fr) minmax(90px, 42%) 58px;
align-items: center;
gap: 8px;
min-height: 24px;
}
.day-platform {
color: #334155;
font-size: 13px;
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.day-bar {
height: 9px;
border-radius: 999px;
background: #eef2f7;
overflow: hidden;
}
.day-bar-inner {
height: 100%;
min-width: 3px;
border-radius: 999px;
}
.day-count {
color: #111827;
font-size: 13px;
font-weight: 700;
text-align: right;
}
.chart-empty {
display: flex;
align-items: center;
justify-content: center;
min-height: 220px;
color: #6b7280;
font-size: 14px;
}
.trend-summary {
margin-top: 16px;
padding: 16px;
border: 1px solid #edf2f7;
border-radius: 12px;
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
}
.trend-summary-title {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.trend-summary-title-main {
color: #111827;
font-size: 16px;
font-weight: 800;
}
.trend-summary-actions {
display: flex;
align-items: center;
gap: 10px;
}
.trend-summary-range {
color: #64748b;
font-size: 13px;
font-weight: 700;
}
.trend-switch {
display: inline-grid;
grid-template-columns: 1fr 1fr;
gap: 2px;
padding: 3px;
border: 1px solid #d7dce3;
border-radius: 10px;
background: #f1f5f9;
}
.trend-switch-btn {
border: none;
border-radius: 7px;
background: transparent;
color: #64748b;
padding: 6px 11px;
cursor: pointer;
font-size: 13px;
font-weight: 800;
}
.trend-switch-btn.active {
background: #2563eb;
color: #fff;
box-shadow: 0 8px 16px rgba(37, 99, 235, 0.18);
}
.trend-summary svg {
display: block;
width: 100%;
height: 220px;
}
.trend-label {
fill: #64748b;
font-size: 12px;
font-weight: 700;
}
.trend-total-label {
fill: #2563eb;
font-size: 12px;
font-weight: 800;
}
.trend-hover-hit {
cursor: crosshair;
}
.trend-tooltip {
position: fixed;
z-index: 30;
display: none;
min-width: 260px;
max-width: 360px;
max-height: 420px;
overflow-y: auto;
padding: 10px 12px;
border: 1px solid rgba(148, 163, 184, 0.28);
border-radius: 10px;
background: rgba(15, 23, 42, 0.94);
color: #fff;
box-shadow: 0 16px 34px rgba(15, 23, 42, 0.24);
pointer-events: none;
}
.trend-tooltip-title {
margin-bottom: 8px;
font-size: 14px;
font-weight: 800;
}
.trend-tooltip-total {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
color: #cbd5e1;
font-size: 12px;
}
.trend-tooltip-total strong {
color: #fff;
font-size: 13px;
}
.trend-tooltip-list {
display: grid;
gap: 4px;
padding-top: 6px;
border-top: 1px solid rgba(148, 163, 184, 0.22);
}
.trend-tooltip-row {
display: grid;
grid-template-columns: 10px minmax(0, 1fr) auto;
align-items: center;
gap: 8px;
color: #e5e7eb;
font-size: 12px;
line-height: 22px;
}
.trend-tooltip-dot {
width: 9px;
height: 9px;
border-radius: 999px;
}
.trend-tooltip-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.trend-tooltip-count {
color: #fff;
font-weight: 800;
}
table {
width: 100%;
border-collapse: collapse;
}
thead {
background: #f8fafc;
}
th, td {
padding: 15px 18px;
border-bottom: 1px solid #edf0f5;
text-align: left;
font-size: 14px;
}
th {
font-weight: 600;
color: #64748b;
}
tbody tr:hover {
background: #f8fafc;
}
.platform,
.num {
font-weight: 700;
}
.num-ok {
color: #16a34a;
}
.num-warn {
color: #f59e0b;
}
.num-danger {
color: #dc2626;
}
.status {
display: inline-block;
padding: 4px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
}
.status-ok {
background: #dcfce7;
color: #166534;
}
.status-warn {
background: #fef3c7;
color: #92400e;
}
.status-danger {
background: #fee2e2;
color: #991b1b;
}
.error {
display: none;
margin-top: 16px;
padding: 12px 14px;
border-radius: 8px;
background: #fee2e2;
color: #991b1b;
font-size: 14px;
}
@media (max-width: 900px) {
.layout {
grid-template-columns: 1fr;
}
.sidebar {
display: flex;
gap: 10px;
padding: 14px;
box-shadow: none;
}
.nav-btn {
justify-content: center;
margin-bottom: 0;
}
.summary {
grid-template-columns: repeat(2, 1fr);
}
.header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
}
@media (max-width: 600px) {
.main {
padding: 14px;
}
.summary {
grid-template-columns: 1fr;
}
th, td {
padding: 10px;
font-size: 13px;
}
.field,
.field input,
.filter-row .btn,
.quick-range {
width: 100%;
}
.quick-btn {
flex: 1;
}
}
const API_URL = "/api/queue/status";
const STATS_API_URL = "/api/queue/platformStats";
const platforms = [
"BDAI", "DB", "DOUBA", "DP", "DPA", "DYAI",
"KIMI", "TXYB", "TXYBA", "TYQW", "TYQWA", "WXYY"
];
let currentPanel = "queue";
let loadedPanels = {queue: false, trend: false};
let trendChartMode = "platform";
let lastTrendData = null;
function showPanel(panel) {
currentPanel = panel;
document.getElementById("queuePanel").classList.toggle("active", panel === "queue");
document.getElementById("trendPanel").classList.toggle("active", panel === "trend");
document.getElementById("queueNavBtn").classList.toggle("active", panel === "queue");
document.getElementById("trendNavBtn").classList.toggle("active", panel === "trend");
document.getElementById("pageTitle").innerText = panel === "queue"
? "GEO 队列监控面板"
: "GEO历史任务数量趋势";
document.getElementById("pageDesc").innerText = panel === "queue"
? "查看各平台 stream_batch / batch 队列积压情况"
: "按时间范围查看各平台历史任务趋势";
hideError();
if (!loadedPanels[panel]) {
refreshCurrentPanel();
}
}
function refreshCurrentPanel() {
if (currentPanel === "queue") {
loadQueueStatus();
} else {
loadPlatformStats();
}
}
function hideError() {
const errorBox = document.getElementById("errorBox");
errorBox.style.display = "none";
errorBox.innerText = "";
}
function showError(message) {
const errorBox = document.getElementById("errorBox");
errorBox.style.display = "block";
errorBox.innerText = message;
}
function setTrendLoading(isLoading) {
document.querySelector(".trend-panel").classList.toggle("loading", isLoading);
}
function getNumClass(num) {
if (num === 0) {
return "num-ok";
}
if (num < 500) {
return "num-warn";
}
return "num-danger";
}
function getStatus(total) {
if (total === 0) {
return {text: "正常", className: "status status-ok"};
}
if (total < 500) {
return {text: "轻微积压", className: "status status-warn"};
}
return {text: "严重积压", className: "status status-danger"};
}
function renderData(data) {
const tbody = document.getElementById("queueTableBody");
let totalCount = 0;
let streamBatchCount = 0;
let batchCount = 0;
tbody.innerHTML = "";
platforms.forEach(platform => {
const streamKey = `${platform}:geo:stream_batch:list`;
const batchKey = `${platform}:geo:batch:list`;
const streamValue = Number(data[streamKey] || 0);
const batchValue = Number(data[batchKey] || 0);
const platformTotal = streamValue + batchValue;
const status = getStatus(platformTotal);
const tr = document.createElement("tr");
streamBatchCount += streamValue;
batchCount += batchValue;
totalCount += platformTotal;
tr.innerHTML = `
<td class="platform">${platform}</td>
<td class="num ${getNumClass(streamValue)}">${streamValue}</td>
<td class="num ${getNumClass(batchValue)}">${batchValue}</td>
<td class="num ${getNumClass(platformTotal)}">${platformTotal}</td>
<td><span class="${status.className}">${status.text}</span></td>
`;
tbody.appendChild(tr);
});
document.getElementById("totalCount").innerText = totalCount;
document.getElementById("streamBatchCount").innerText = streamBatchCount;
document.getElementById("batchCount").innerText = batchCount;
document.getElementById("commitCount").innerText = Number(data["geo:task_commit:list"] || 0);
document.getElementById("lastTime").innerText = "最后刷新:" + new Date().toLocaleString();
}
function getStatsItems(responseData) {
if (Array.isArray(responseData)) {
return responseData;
}
if (responseData && Array.isArray(responseData.data)) {
return responseData.data;
}
return [];
}
function normalizeStats(responseData) {
return getStatsItems(responseData)
.map(item => ({
time: normalizeStatDate(item["时间"] || item.time || item.date || item.begin || ""),
platform: item["平台"] || item.platform || item.name || "未知平台",
count: Number(item["数量"] || item.count || item.value || 0)
}))
.filter(item => item.time && Number.isFinite(item.count));
}
function normalizeStatDate(value) {
return String(value || "").slice(0, 10);
}
function createSvgEl(tag, attrs = {}) {
const el = document.createElementNS("http://www.w3.org/2000/svg", tag);
Object.keys(attrs).forEach(key => el.setAttribute(key, attrs[key]));
return el;
}
function renderPlatformTrend(responseData) {
const chart = document.getElementById("platformTrendChart");
const summary = document.getElementById("trendSummary");
const statsNote = document.getElementById("statsNote");
const colors = ["#2563eb", "#16a34a", "#f59e0b", "#dc2626", "#7c3aed", "#0891b2", "#db2777", "#475569"];
const items = normalizeStats(responseData);
chart.innerHTML = "";
summary.innerHTML = "";
if (!items.length) {
statsNote.innerText = "暂无趋势数据";
chart.innerHTML = '<div class="chart-empty">当前时间范围暂无历史任务趋势数据</div>';
return;
}
const chartLabels = [...new Set(items.map(item => item.time))].sort();
const cardLabels = [...chartLabels].reverse();
const oldestDate = chartLabels[0];
const newestDate = chartLabels[chartLabels.length - 1];
const platformTotals = new Map();
const pointMap = new Map();
items.forEach(item => {
platformTotals.set(item.platform, (platformTotals.get(item.platform) || 0) + item.count);
const pointKey = `${item.platform}__${item.time}`;
pointMap.set(pointKey, (pointMap.get(pointKey) || 0) + item.count);
});
const platformList = [...platformTotals.entries()]
.sort((a, b) => b[1] - a[1])
.map(([platform]) => platform);
const maxCount = Math.max(...pointMap.values(), 1);
const platformColorMap = new Map(platformList.map((platform, index) => [platform, colors[index % colors.length]]));
const dayTotals = chartLabels.map(label => ({
date: label,
total: platformList.reduce((sum, platform) => sum + (pointMap.get(`${platform}__${label}`) || 0), 0)
}));
lastTrendData = {
labels: chartLabels,
platformList,
pointMap,
platformColorMap,
dayTotals
};
const grid = document.createElement("div");
grid.className = "daily-grid";
cardLabels.forEach(label => {
const dayItems = platformList.map(platform => ({
platform,
value: pointMap.get(`${platform}__${label}`) || 0,
color: platformColorMap.get(platform)
}));
const dayTotal = dayItems.reduce((sum, item) => sum + item.value, 0);
const card = document.createElement("div");
const head = document.createElement("div");
const date = document.createElement("div");
const total = document.createElement("div");
const list = document.createElement("div");
card.className = "day-card";
head.className = "day-card-head";
date.className = "day-date";
total.className = "day-total";
list.className = "day-list";
date.innerText = label;
total.innerText = `总量 ${dayTotal}`;
head.appendChild(date);
head.appendChild(total);
dayItems.forEach(item => {
const row = document.createElement("div");
const name = document.createElement("div");
const bar = document.createElement("div");
const barInner = document.createElement("div");
const count = document.createElement("div");
row.className = "day-row";
name.className = "day-platform";
bar.className = "day-bar";
barInner.className = "day-bar-inner";
count.className = "day-count";
name.innerText = item.platform;
barInner.style.width = `${Math.max(2, Math.round((item.value / maxCount) * 100))}%`;
barInner.style.background = item.color;
count.innerText = item.value;
bar.appendChild(barInner);
row.appendChild(name);
row.appendChild(bar);
row.appendChild(count);
list.appendChild(row);
});
card.appendChild(head);
card.appendChild(list);
grid.appendChild(card);
});
chart.appendChild(grid);
renderTrendSummary();
statsNote.innerText = `共 ${platformList.length} 个平台,${oldestDate}${newestDate}`;
document.getElementById("lastTime").innerText = "最后刷新:" + new Date().toLocaleString();
}
function renderTrendSummary() {
if (!lastTrendData) {
return;
}
const summary = document.getElementById("trendSummary");
const titleText = trendChartMode === "total" ? "每日总量趋势" : "各平台每日趋势";
const series = trendChartMode === "total"
? [{
name: "总量",
color: "#2563eb",
values: lastTrendData.dayTotals.map(item => item.total)
}]
: lastTrendData.platformList.map(platform => ({
name: platform,
color: lastTrendData.platformColorMap.get(platform),
values: lastTrendData.labels.map(label => lastTrendData.pointMap.get(`${platform}__${label}`) || 0)
}));
const maxValue = Math.max(...series.flatMap(item => item.values), 1);
const width = Math.max(summary.clientWidth - 32, 720);
const height = 220;
const padding = {top: 26, right: 18, bottom: 40, left: 52};
const plotWidth = width - padding.left - padding.right;
const plotHeight = height - padding.top - padding.bottom;
const xScale = index => lastTrendData.labels.length > 1
? padding.left + (plotWidth / (lastTrendData.labels.length - 1)) * index
: padding.left + plotWidth / 2;
const yScale = value => padding.top + plotHeight - (value / maxValue) * plotHeight;
const head = document.createElement("div");
const title = document.createElement("span");
const actions = document.createElement("div");
const range = document.createElement("span");
const switchWrap = document.createElement("div");
const platformBtn = document.createElement("button");
const totalBtn = document.createElement("button");
const svg = createSvgEl("svg", {viewBox: `0 0 ${width} ${height}`, role: "img"});
const tooltip = document.createElement("div");
summary.innerHTML = "";
head.className = "trend-summary-title";
title.className = "trend-summary-title-main";
actions.className = "trend-summary-actions";
range.className = "trend-summary-range";
switchWrap.className = "trend-switch";
platformBtn.className = `trend-switch-btn ${trendChartMode === "platform" ? "active" : ""}`;
totalBtn.className = `trend-switch-btn ${trendChartMode === "total" ? "active" : ""}`;
title.innerText = titleText;
range.innerText = `${lastTrendData.labels[lastTrendData.labels.length - 1]}${lastTrendData.labels[0]}`;
platformBtn.innerText = "平台";
totalBtn.innerText = "总量";
platformBtn.onclick = () => setTrendChartMode("platform");
totalBtn.onclick = () => setTrendChartMode("total");
switchWrap.appendChild(platformBtn);
switchWrap.appendChild(totalBtn);
head.appendChild(title);
actions.appendChild(range);
actions.appendChild(switchWrap);
head.appendChild(actions);
tooltip.className = "trend-tooltip";
for (let i = 0; i <= 3; i++) {
const value = Math.round((maxValue / 3) * i);
const y = yScale(value);
svg.appendChild(createSvgEl("line", {
x1: padding.left,
y1: y,
x2: width - padding.right,
y2: y,
stroke: i === 0 ? "#cbd5e1" : "#e5e7eb",
"stroke-width": "1"
}));
const axisText = createSvgEl("text", {
x: padding.left - 10,
y: y + 4,
"text-anchor": "end",
class: "trend-label"
});
axisText.textContent = value;
svg.appendChild(axisText);
}
series.forEach(line => {
const points = line.values.map((value, index) => ({
date: lastTrendData.labels[index],
value,
x: xScale(index),
y: yScale(value)
}));
svg.appendChild(createSvgEl("polyline", {
points: points.map(point => `${point.x},${point.y}`).join(" "),
fill: "none",
stroke: line.color,
"stroke-width": trendChartMode === "total" ? "3" : "2",
"stroke-opacity": trendChartMode === "total" ? "1" : "0.78",
"stroke-linecap": "round",
"stroke-linejoin": "round"
}));
points.forEach(point => {
svg.appendChild(createSvgEl("circle", {
cx: point.x,
cy: point.y,
r: trendChartMode === "total" ? "5" : "3",
fill: line.color,
stroke: "#fff",
"stroke-width": "2"
}));
if (trendChartMode === "total") {
const totalText = createSvgEl("text", {
x: point.x,
y: Math.max(14, point.y - 10),
"text-anchor": "middle",
class: "trend-total-label"
});
totalText.textContent = point.value;
svg.appendChild(totalText);
}
});
});
lastTrendData.labels.forEach((label, index) => {
const dateText = createSvgEl("text", {
x: xScale(index),
y: height - 12,
"text-anchor": "middle",
class: "trend-label"
});
dateText.textContent = label.slice(5);
svg.appendChild(dateText);
});
lastTrendData.labels.forEach((label, index) => {
const left = index === 0 ? padding.left : (xScale(index - 1) + xScale(index)) / 2;
const right = index === lastTrendData.labels.length - 1
? width - padding.right
: (xScale(index) + xScale(index + 1)) / 2;
const hit = createSvgEl("rect", {
x: left,
y: padding.top,
width: Math.max(12, right - left),
height: plotHeight,
fill: "transparent",
"pointer-events": "all",
class: "trend-hover-hit"
});
hit.addEventListener("mouseenter", event => showTrendDayTooltip(event, label));
hit.addEventListener("mousemove", moveTrendDayTooltip);
hit.addEventListener("mouseleave", hideTrendDayTooltip);
svg.appendChild(hit);
});
summary.appendChild(head);
summary.appendChild(svg);
summary.appendChild(tooltip);
}
function setTrendChartMode(mode) {
trendChartMode = mode;
renderTrendSummary();
}
function moveTrendDayTooltip(event) {
const tooltip = document.querySelector(".trend-tooltip");
if (!tooltip) {
return;
}
const offset = 14;
const tooltipWidth = tooltip.offsetWidth || 280;
const tooltipHeight = tooltip.offsetHeight || 120;
let left = event.clientX + offset;
let top = event.clientY + offset;
if (left + tooltipWidth > window.innerWidth - 12) {
left = event.clientX - tooltipWidth - offset;
}
if (top + tooltipHeight > window.innerHeight - 12) {
top = event.clientY - tooltipHeight - offset;
}
tooltip.style.left = `${Math.max(12, left)}px`;
tooltip.style.top = `${Math.max(12, top)}px`;
}
function showTrendDayTooltip(event, date) {
const tooltip = document.querySelector(".trend-tooltip");
if (!tooltip || !lastTrendData) {
return;
}
const dayItems = lastTrendData.platformList.map(platform => ({
platform,
value: lastTrendData.pointMap.get(`${platform}__${date}`) || 0,
color: lastTrendData.platformColorMap.get(platform)
})).sort((a, b) => b.value - a.value);
const dayTotal = dayItems.reduce((sum, item) => sum + item.value, 0);
tooltip.innerHTML = "";
const title = document.createElement("div");
const total = document.createElement("div");
const totalValue = document.createElement("strong");
const list = document.createElement("div");
title.className = "trend-tooltip-title";
title.innerText = date;
total.className = "trend-tooltip-total";
total.innerHTML = "<span>当天总量</span>";
totalValue.innerText = dayTotal;
total.appendChild(totalValue);
list.className = "trend-tooltip-list";
dayItems.forEach(item => {
const row = document.createElement("div");
const dot = document.createElement("span");
const name = document.createElement("span");
const count = document.createElement("span");
row.className = "trend-tooltip-row";
dot.className = "trend-tooltip-dot";
dot.style.background = item.color;
name.className = "trend-tooltip-name";
name.innerText = item.platform;
count.className = "trend-tooltip-count";
count.innerText = item.value;
row.appendChild(dot);
row.appendChild(name);
row.appendChild(count);
list.appendChild(row);
});
tooltip.appendChild(title);
tooltip.appendChild(total);
tooltip.appendChild(list);
tooltip.style.display = "block";
moveTrendDayTooltip(event);
}
function hideTrendDayTooltip() {
const tooltip = document.querySelector(".trend-tooltip");
if (tooltip) {
tooltip.style.display = "none";
}
}
function toDateInputValue(date) {
const offsetDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000);
return offsetDate.toISOString().slice(0, 10);
}
function setQuickRange(days, shouldLoad = true) {
const beginInput = document.getElementById("beginTime");
const endInput = document.getElementById("endTime");
const now = new Date();
const start = new Date(now);
start.setDate(start.getDate() - days + 1);
start.setHours(0, 0, 0, 0);
beginInput.value = toDateInputValue(start);
endInput.value = toDateInputValue(now);
document.getElementById("range3Btn").classList.toggle("active", days === 3);
document.getElementById("range7Btn").classList.toggle("active", days === 7);
document.getElementById("range30Btn").classList.toggle("active", days === 30);
if (shouldLoad && currentPanel === "trend") {
loadPlatformStats();
} else if (shouldLoad) {
loadedPanels.trend = false;
}
}
function clearQuickRangeActive() {
document.getElementById("range3Btn").classList.remove("active");
document.getElementById("range7Btn").classList.remove("active");
document.getElementById("range30Btn").classList.remove("active");
}
function handleDateRangeChange() {
clearQuickRangeActive();
loadedPanels.trend = false;
if (currentPanel === "trend") {
loadPlatformStats();
}
}
function buildStatsUrl() {
const begin = document.getElementById("beginTime").value;
const end = document.getElementById("endTime").value;
if (!begin || !end) {
throw new Error("请选择开始时间和结束时间");
}
if (begin > end) {
throw new Error("开始时间不能晚于结束时间");
}
return `${STATS_API_URL}?${new URLSearchParams({begin, end}).toString()}`;
}
async function fetchJson(url) {
const res = await fetch(url, {method: "GET", cache: "no-store"});
if (!res.ok) {
throw new Error("接口请求失败,状态码:" + res.status);
}
return res.json();
}
async function loadQueueStatus() {
hideError();
try {
renderData(await fetchJson(API_URL));
loadedPanels.queue = true;
} catch (e) {
showError("队列数据加载失败:" + e.message);
}
}
async function loadPlatformStats() {
const statsNote = document.getElementById("statsNote");
hideError();
statsNote.innerText = "加载中";
trendChartMode = "platform";
setTrendLoading(true);
try {
renderPlatformTrend(await fetchJson(buildStatsUrl()));
loadedPanels.trend = true;
} catch (e) {
statsNote.innerText = "加载失败";
showError("历史任务趋势加载失败:" + e.message);
} finally {
setTrendLoading(false);
}
}
document.getElementById("beginTime").addEventListener("change", handleDateRangeChange);
document.getElementById("endTime").addEventListener("change", handleDateRangeChange);
setQuickRange(3, false);
loadQueueStatus();
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment