添加数据分组,按天,近10天,近30天进行分类。
This commit is contained in:
+130
-23
@@ -28,6 +28,7 @@
|
||||
let historyByAssignment = {};
|
||||
let assignmentNames = {};
|
||||
let selectedAssignmentId = '';
|
||||
let selectedRange = 'today';
|
||||
let myChart = null;
|
||||
let timerId = null;
|
||||
let isProcessing = false;
|
||||
@@ -60,18 +61,43 @@
|
||||
};
|
||||
}
|
||||
|
||||
function getDayKey(timestamp) {
|
||||
const date = new Date(Number(timestamp));
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function normalizeHistory(raw) {
|
||||
const normalized = {};
|
||||
if (!raw || typeof raw !== 'object') return normalized;
|
||||
Object.entries(raw).forEach(([assignmentId, points]) => {
|
||||
if (!assignmentId || !Array.isArray(points)) return;
|
||||
const unique = new Map();
|
||||
points.map(normalizePoint).filter(Boolean).forEach(point => unique.set(point.timestamp, point));
|
||||
normalized[assignmentId] = Array.from(unique.values()).sort((a, b) => a.timestamp - b.timestamp);
|
||||
Object.entries(raw).forEach(([assignmentId, storedDays]) => {
|
||||
if (!assignmentId || !storedDays || typeof storedDays !== 'object') return;
|
||||
const days = {};
|
||||
const sourceDays = Array.isArray(storedDays)
|
||||
? storedDays.reduce((result, point) => {
|
||||
const dayKey = point && Number.isFinite(Number(point.timestamp)) ? getDayKey(point.timestamp) : '';
|
||||
if (dayKey) (result[dayKey] ||= []).push(point);
|
||||
return result;
|
||||
}, {})
|
||||
: storedDays;
|
||||
Object.entries(sourceDays).forEach(([dayKey, points]) => {
|
||||
if (!Array.isArray(points)) return;
|
||||
const unique = new Map();
|
||||
points.map(normalizePoint).filter(Boolean).forEach(point => unique.set(point.timestamp, point));
|
||||
if (unique.size) days[dayKey] = Array.from(unique.values()).sort((a, b) => a.timestamp - b.timestamp);
|
||||
});
|
||||
if (Object.keys(days).length) normalized[assignmentId] = days;
|
||||
});
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function getAssignmentPoints(assignmentId) {
|
||||
const days = historyByAssignment[assignmentId] || {};
|
||||
return Object.values(days).flat().sort((a, b) => a.timestamp - b.timestamp);
|
||||
}
|
||||
|
||||
async function loadHistory() {
|
||||
historyByAssignment = normalizeHistory(await GM_getValue(STORAGE_KEY, {}));
|
||||
}
|
||||
@@ -120,13 +146,31 @@
|
||||
backgroundColor: palette.surface, color: palette.text, padding: '15px', borderRadius: '10px',
|
||||
boxShadow: palette.shadow, zIndex: '9999',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
||||
fontSize: '14px', border: `1px solid ${palette.border}`, display: 'flex', flexDirection: 'column', overflow: 'hidden'
|
||||
fontSize: '14px', border: `1px solid ${palette.border}`, boxSizing: 'border-box', display: 'flex', flexDirection: 'column', overflow: 'hidden'
|
||||
});
|
||||
const header = document.createElement('div');
|
||||
header.style.cssText = `display:flex;align-items:center;gap:8px;margin-bottom:8px;flex-shrink:0;color:${palette.text}`;
|
||||
header.innerHTML = `<strong style="letter-spacing:.02em">用量监控</strong><select id="assignment-selector" style="min-width:0;flex:1;background:${palette.control};color:${palette.text};border:1px solid ${palette.border};border-radius:5px;padding:4px 6px"></select>`;
|
||||
header.innerHTML = `<strong style="letter-spacing:.02em;white-space:nowrap">用量监控</strong><select id="assignment-selector" style="min-width:0;flex:1;background:${palette.control};color:${palette.text};border:1px solid ${palette.border};border-radius:5px;padding:4px 6px"></select>`;
|
||||
container.appendChild(header);
|
||||
|
||||
const rangeBar = document.createElement('div');
|
||||
rangeBar.id = 'chart-range-selector';
|
||||
rangeBar.style.cssText = 'display:flex;gap:6px;margin-bottom:7px;flex-shrink:0';
|
||||
[['today', '今天'], ['7d', '最近 7 天'], ['30d', '最近 30 天']].forEach(([value, label]) => {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.dataset.range = value;
|
||||
button.textContent = label;
|
||||
button.style.cssText = `background:${palette.control};color:${palette.text};border:1px solid ${palette.border};border-radius:5px;padding:3px 7px;font-size:12px;cursor:pointer`;
|
||||
button.addEventListener('click', () => {
|
||||
selectedRange = value;
|
||||
updateRangeButtons();
|
||||
updateChart();
|
||||
});
|
||||
rangeBar.appendChild(button);
|
||||
});
|
||||
container.appendChild(rangeBar);
|
||||
|
||||
const status = document.createElement('div');
|
||||
status.id = 'monitor-status';
|
||||
status.textContent = '初始化...';
|
||||
@@ -155,6 +199,7 @@
|
||||
});
|
||||
refreshAssignmentOptions();
|
||||
initChart();
|
||||
updateRangeButtons();
|
||||
}
|
||||
|
||||
function makeDraggable(container, handle) {
|
||||
@@ -203,10 +248,13 @@
|
||||
});
|
||||
window.addEventListener('mousemove', event => {
|
||||
if (!resizing) return;
|
||||
const maxWidth = Math.max(PANEL_MIN_WIDTH, window.innerWidth - container.getBoundingClientRect().left);
|
||||
const maxHeight = Math.max(PANEL_MIN_HEIGHT, window.innerHeight - container.getBoundingClientRect().top);
|
||||
const width = Math.min(Math.max(PANEL_MIN_WIDTH, startWidth + event.clientX - startX), maxWidth);
|
||||
const height = Math.min(Math.max(PANEL_MIN_HEIGHT, startHeight + event.clientY - startY), maxHeight);
|
||||
const rect = container.getBoundingClientRect();
|
||||
const minWidth = Math.min(PANEL_MIN_WIDTH, window.innerWidth);
|
||||
const minHeight = Math.min(PANEL_MIN_HEIGHT, window.innerHeight);
|
||||
const maxWidth = Math.max(minWidth, window.innerWidth - rect.left);
|
||||
const maxHeight = Math.max(minHeight, window.innerHeight - rect.top);
|
||||
const width = Math.min(Math.max(minWidth, startWidth + event.clientX - startX), maxWidth);
|
||||
const height = Math.min(Math.max(minHeight, startHeight + event.clientY - startY), maxHeight);
|
||||
container.style.width = `${width}px`;
|
||||
container.style.height = `${height}px`;
|
||||
myChart?.resize();
|
||||
@@ -261,11 +309,45 @@
|
||||
});
|
||||
}
|
||||
|
||||
function startOfToday() {
|
||||
const date = new Date();
|
||||
date.setHours(0, 0, 0, 0);
|
||||
return date.getTime();
|
||||
}
|
||||
|
||||
function aggregatePoints(points, intervalMs) {
|
||||
const buckets = new Map();
|
||||
points.forEach(point => {
|
||||
const bucket = Math.floor(point.timestamp / intervalMs) * intervalMs;
|
||||
// 额度为累计值,取每个时间桶最后一次采样,保留该时间段结束时的真实状态。
|
||||
const previous = buckets.get(bucket);
|
||||
if (!previous || point.timestamp > previous.timestamp) buckets.set(bucket, point);
|
||||
});
|
||||
return Array.from(buckets.values()).sort((a, b) => a.timestamp - b.timestamp);
|
||||
}
|
||||
|
||||
function getDisplayedPoints(assignmentId) {
|
||||
const allPoints = getAssignmentPoints(assignmentId);
|
||||
const now = Date.now();
|
||||
if (selectedRange === 'today') return allPoints.filter(point => point.timestamp >= startOfToday());
|
||||
if (selectedRange === '7d') return aggregatePoints(allPoints.filter(point => point.timestamp >= now - 7 * 86400000), 3600000);
|
||||
return aggregatePoints(allPoints.filter(point => point.timestamp >= now - 30 * 86400000), 6 * 3600000);
|
||||
}
|
||||
|
||||
function getPointsWithDelta(assignmentId) {
|
||||
const points = historyByAssignment[assignmentId] || [];
|
||||
const points = getDisplayedPoints(assignmentId);
|
||||
return points.map((point, index) => ({ ...point, delta: index ? point.used - points[index - 1].used : 0 }));
|
||||
}
|
||||
|
||||
function updateRangeButtons() {
|
||||
document.querySelectorAll('#chart-range-selector button').forEach(button => {
|
||||
const active = button.dataset.range === selectedRange;
|
||||
button.style.background = active ? '#0ea5e9' : '';
|
||||
button.style.borderColor = active ? '#7dd3fc' : '';
|
||||
button.style.fontWeight = active ? '700' : '400';
|
||||
});
|
||||
}
|
||||
|
||||
function updateChart() {
|
||||
if (!myChart) return;
|
||||
const points = getPointsWithDelta(selectedAssignmentId);
|
||||
@@ -292,9 +374,11 @@
|
||||
const used = numberOrNull(quota && quota.used);
|
||||
if (!assignmentId || used === null) return false;
|
||||
const point = normalizePoint({ timestamp: Date.now(), used, total: quota.total, remaining: quota.remaining, unlimited: quota.unlimited, unit: quota.unit, quotaUpdatedAt: quota.updatedAt });
|
||||
historyByAssignment[assignmentId] = historyByAssignment[assignmentId] || [];
|
||||
historyByAssignment[assignmentId].push(point);
|
||||
historyByAssignment[assignmentId].sort((a, b) => a.timestamp - b.timestamp);
|
||||
const dayKey = getDayKey(point.timestamp);
|
||||
historyByAssignment[assignmentId] = historyByAssignment[assignmentId] || {};
|
||||
historyByAssignment[assignmentId][dayKey] = historyByAssignment[assignmentId][dayKey] || [];
|
||||
historyByAssignment[assignmentId][dayKey].push(point);
|
||||
historyByAssignment[assignmentId][dayKey].sort((a, b) => a.timestamp - b.timestamp);
|
||||
assignmentNames[assignmentId] = assignmentNames[assignmentId] || assignmentId;
|
||||
if (!selectedAssignmentId) selectedAssignmentId = assignmentId;
|
||||
refreshAssignmentOptions();
|
||||
@@ -402,8 +486,15 @@
|
||||
}
|
||||
|
||||
function buildExportPayload() {
|
||||
const assignments = Object.entries(historyByAssignment).map(([id, points]) => [id, assignmentNames[id] || '', points.map(point => [point.timestamp, point.used, point.total, point.remaining, point.unlimited ? 1 : 0, point.unit, point.quotaUpdatedAt])]);
|
||||
return { v: 1, a: assignments };
|
||||
const assignments = Object.entries(historyByAssignment).map(([id, days]) => [
|
||||
id,
|
||||
assignmentNames[id] || '',
|
||||
Object.entries(days).map(([dayKey, points]) => [
|
||||
dayKey,
|
||||
points.map(point => [point.timestamp, point.used, point.total, point.remaining, point.unlimited ? 1 : 0, point.unit, point.quotaUpdatedAt])
|
||||
])
|
||||
]);
|
||||
return { v: 2, a: assignments };
|
||||
}
|
||||
|
||||
async function writeClipboard(text) {
|
||||
@@ -419,7 +510,7 @@
|
||||
}
|
||||
|
||||
async function exportHistory() {
|
||||
const count = Object.values(historyByAssignment).reduce((sum, points) => sum + points.length, 0);
|
||||
const count = Object.keys(historyByAssignment).reduce((sum, assignmentId) => sum + getAssignmentPoints(assignmentId).length, 0);
|
||||
if (!count) throw new Error('没有可导出的历史记录');
|
||||
const compressed = await gzipText(JSON.stringify(buildExportPayload()));
|
||||
const text = EXPORT_PREFIX + bytesToBase64(compressed);
|
||||
@@ -428,18 +519,27 @@
|
||||
}
|
||||
|
||||
function mergeImportPayload(payload) {
|
||||
if (!payload || payload.v !== 1 || !Array.isArray(payload.a)) throw new Error('历史数据格式无效');
|
||||
if (!payload || ![1, 2].includes(payload.v) || !Array.isArray(payload.a)) throw new Error('历史数据格式无效');
|
||||
let imported = 0;
|
||||
payload.a.forEach(entry => {
|
||||
if (!Array.isArray(entry) || typeof entry[0] !== 'string' || !Array.isArray(entry[2])) return;
|
||||
const [assignmentId, name, rows] = entry;
|
||||
const existing = new Map((historyByAssignment[assignmentId] || []).map(point => [point.timestamp, point]));
|
||||
const [assignmentId, name, source] = entry;
|
||||
const rows = payload.v === 1
|
||||
? source
|
||||
: source.flatMap(day => Array.isArray(day) && Array.isArray(day[1]) ? day[1] : []);
|
||||
const existing = new Set(getAssignmentPoints(assignmentId).map(point => point.timestamp));
|
||||
historyByAssignment[assignmentId] = historyByAssignment[assignmentId] || {};
|
||||
rows.forEach(row => {
|
||||
if (!Array.isArray(row)) return;
|
||||
const point = normalizePoint({ timestamp: row[0], used: row[1], total: row[2], remaining: row[3], unlimited: row[4], unit: row[5], quotaUpdatedAt: row[6] });
|
||||
if (point && !existing.has(point.timestamp)) { existing.set(point.timestamp, point); imported += 1; }
|
||||
if (!point || existing.has(point.timestamp)) return;
|
||||
const dayKey = getDayKey(point.timestamp);
|
||||
historyByAssignment[assignmentId][dayKey] = historyByAssignment[assignmentId][dayKey] || [];
|
||||
historyByAssignment[assignmentId][dayKey].push(point);
|
||||
existing.add(point.timestamp);
|
||||
imported += 1;
|
||||
});
|
||||
historyByAssignment[assignmentId] = Array.from(existing.values()).sort((a, b) => a.timestamp - b.timestamp);
|
||||
Object.values(historyByAssignment[assignmentId]).forEach(points => points.sort((a, b) => a.timestamp - b.timestamp));
|
||||
assignmentNames[assignmentId] = assignmentNames[assignmentId] || name || assignmentId;
|
||||
});
|
||||
return imported;
|
||||
@@ -492,11 +592,18 @@
|
||||
window.addEventListener('resize', () => {
|
||||
const container = document.getElementById('usage-monitor-container');
|
||||
if (!container) return;
|
||||
const maxWidth = Math.max(PANEL_MIN_WIDTH, window.innerWidth - 20);
|
||||
const maxHeight = Math.max(PANEL_MIN_HEIGHT, window.innerHeight - 20);
|
||||
const width = Math.min(container.getBoundingClientRect().width, maxWidth);
|
||||
const height = Math.min(container.getBoundingClientRect().height, maxHeight);
|
||||
container.style.width = `${width}px`;
|
||||
container.style.height = `${height}px`;
|
||||
const rect = container.getBoundingClientRect();
|
||||
container.style.left = `${Math.min(Math.max(0, rect.left), Math.max(0, window.innerWidth - rect.width))}px`;
|
||||
container.style.top = `${Math.min(Math.max(0, rect.top), Math.max(0, window.innerHeight - rect.height))}px`;
|
||||
container.style.right = 'auto';
|
||||
myChart?.resize();
|
||||
myChart?.update('none');
|
||||
});
|
||||
window.addEventListener('beforeunload', () => clearTimeout(timerId));
|
||||
console.log('[用量监控] 脚本 V5.0 已启动。');
|
||||
|
||||
Reference in New Issue
Block a user