Files
gemini-banlancer/web/static/js/status-grid.js
2025-11-20 12:24:05 +08:00

226 lines
9.4 KiB
JavaScript

// Filename: web/static/js/status-grid.js (V6.0 - 健壮初始化最终版)
// export default function initializeStatusGrid() {
class StatusGrid {
constructor(canvasId) {
this.canvas = document.getElementById(canvasId);
if (!this.canvas) { return; }
this.ctx = this.canvas.getContext('2d');
this.config = {
cols: 100, rows: 4, gap: 2, cornerRadius: 2,
colors: {
'ACTIVE': '#22C55E', 'ACTIVE_BLINK': '#A7F3D0',
'PENDING': '#9CA3AF', 'COOLDOWN': '#EAB308',
'DISABLED': '#F97316', 'BANNED': '#EF4444',
'EMPTY': '#F3F4F6',
},
blinkInterval: 100, blinksPerInterval: 2, blinkDuration: 200,
statusOrder: ['ACTIVE', 'COOLDOWN', 'DISABLED', 'BANNED', 'PENDING']
};
this.state = {
squares: [], squareSize: 0,
devicePixelRatio: window.devicePixelRatio || 1,
animationFrameId: null, blinkIntervalId: null,
blinkingSquares: new Set()
};
this.debouncedResize = this.debounce(this.resize.bind(this), 250);
}
init() {
this.setupCanvas();
// 初始化时,绘制一个完全为空的网格
this.state.squares = Array(this.config.rows * this.config.cols).fill({ status: 'EMPTY' });
this.drawFullGrid();
window.addEventListener('resize', this.debouncedResize);
}
/**
* [灵魂重塑] 这是被彻底重写的核心函数
* @param {object} keyStatusCounts - 例如 { "BANNED": 1, "DISABLED": 4, ... }
*/
updateData(keyStatusCounts) {
if (!keyStatusCounts) return;
this.destroyAnimation();
this.state.squares = [];
this._activeIndices = null;
this.state.blinkingSquares.clear();
const totalKeys = Object.values(keyStatusCounts).reduce((s, c) => s + c, 0);
const totalSquares = this.config.rows * this.config.cols;
// 如果没有密钥,则显示为空白的网格
if (totalKeys === 0) {
this.init();
return;
}
let statusMap = [];
let calculatedSquares = 0;
// 1. 严格按照比例,计算每个状态应该占据多少个“像素”
for (const status of this.config.statusOrder) {
const count = keyStatusCounts[status] || 0;
if (count > 0) {
const proportion = count / totalKeys;
const squaresForStatus = Math.floor(proportion * totalSquares);
for (let i = 0; i < squaresForStatus; i++) {
statusMap.push(status);
}
calculatedSquares += squaresForStatus;
}
}
// 2. [关键] 修正四舍五入的误差,将剩余的方块填满
const remainingSquares = totalSquares - calculatedSquares;
if (remainingSquares > 0) {
// 将剩余方块,全部分配给数量最多的那个状态,以使其最不失真
let largestStatus = this.config.statusOrder[0]; // 默认给第一个
let maxCount = -1;
for (const status in keyStatusCounts) {
if (keyStatusCounts[status] > maxCount) {
maxCount = keyStatusCounts[status];
largestStatus = status;
}
}
for (let i = 0; i < remainingSquares; i++) {
statusMap.push(largestStatus);
}
}
// 3. 将最终的、按比例填充的地图,转化为内部的 square 对象
this.state.squares = statusMap.map(status => ({
status: status,
isBlinking: false,
blinkUntil: 0
}));
// 4. [渲染修正] 直接、完整地重绘整个网格,然后启动动画
this.drawFullGrid();
this.startAnimationSystem();
}
// [渲染修正] 一个简单、直接、一次性绘制所有方块的函数
drawFullGrid() {
const rect = this.canvas.getBoundingClientRect();
this.ctx.clearRect(0, 0, rect.width, rect.height);
// offsetX 和 offsetY 是在 setupCanvas 中计算的
const offsetX = this.state.offsetX;
const offsetY = this.state.offsetY;
this.state.squares.forEach((square, i) => {
const c = i % this.config.cols;
const r = Math.floor(i / this.config.cols);
const x = offsetX + c * (this.state.squareSize + this.config.gap);
const y = offsetY + r * (this.state.squareSize + this.config.gap);
const color = this.config.colors[square.status];
this.drawRoundedRect(x, y, this.state.squareSize, this.config.cornerRadius, color);
});
}
startAnimationSystem() {
if(this.state.blinkIntervalId) clearInterval(this.state.blinkIntervalId);
if(this.state.animationFrameId) cancelAnimationFrame(this.state.animationFrameId);
this.state.blinkIntervalId = setInterval(() => {
const activeSquareIndices = this.getActiveSquareIndices();
if (activeSquareIndices.length === 0) return;
for(let i = 0; i < this.config.blinksPerInterval; i++) {
const randomIndex = activeSquareIndices[Math.floor(Math.random() * activeSquareIndices.length)];
const square = this.state.squares[randomIndex];
if (square && !square.isBlinking) {
square.isBlinking = true;
square.blinkUntil = performance.now() + this.config.blinkDuration;
this.state.blinkingSquares.add(randomIndex);
}
}
}, this.config.blinkInterval);
const animationLoop = (timestamp) => {
if (this.state.blinkingSquares.size > 0) {
this.state.blinkingSquares.forEach(index => {
const square = this.state.squares[index];
if(!square) { // 防御性检查
this.state.blinkingSquares.delete(index);
return;
}
const c = index % this.config.cols;
const r = Math.floor(index / this.config.cols);
this.drawSquare(square, c, r);
if (timestamp > square.blinkUntil) {
square.isBlinking = false;
this.state.blinkingSquares.delete(index);
this.drawSquare(square, c, r);
}
});
}
this.state.animationFrameId = requestAnimationFrame(animationLoop);
};
animationLoop();
}
setupCanvas() {
const rect = this.canvas.getBoundingClientRect();
this.canvas.width = rect.width * this.state.devicePixelRatio;
this.canvas.height = rect.height * this.state.devicePixelRatio;
this.ctx.scale(this.state.devicePixelRatio, this.state.devicePixelRatio);
const calculatedWidth = (rect.width - (this.config.cols - 1) * this.config.gap) / this.config.cols;
const calculatedHeight = (rect.height - (this.config.rows - 1) * this.config.gap) / this.config.rows;
this.state.squareSize = Math.max(1, Math.floor(Math.min(calculatedWidth, calculatedHeight)));
const totalGridWidth = this.config.cols * this.state.squareSize + (this.config.cols - 1) * this.config.gap;
const totalGridHeight = this.config.rows * this.state.squareSize + (this.config.rows - 1) * this.config.gap;
this.state.offsetX = Math.floor((rect.width - totalGridWidth) / 2);
this.state.offsetY = Math.floor((rect.height - totalGridHeight) / 2);
}
getActiveSquareIndices() {
if (!this._activeIndices) {
this._activeIndices = [];
this.state.squares.forEach((s, i) => {
if (s.status === 'ACTIVE') this._activeIndices.push(i);
});
}
return this._activeIndices;
}
drawRoundedRect(x, y, size, radius, color) {
this.ctx.fillStyle = color;
this.ctx.beginPath();
this.ctx.moveTo(x + radius, y);
this.ctx.arcTo(x + size, y, x + size, y + size, radius);
this.ctx.arcTo(x + size, y + size, x, y + size, radius);
this.ctx.arcTo(x, y + size, x, y, radius);
this.ctx.arcTo(x, y, x + size, y, radius);
this.ctx.closePath();
this.ctx.fill();
}
drawSquare(square, c, r) {
const x = this.state.offsetX + c * (this.state.squareSize + this.config.gap);
const y = this.state.offsetY + r * (this.state.squareSize + this.config.gap);
const color = square.isBlinking ? this.config.colors.ACTIVE_BLINK : this.config.colors[square.status];
this.drawRoundedRect(x, y, this.state.squareSize, this.config.cornerRadius, color);
}
destroyAnimation() {
if(this.state.animationFrameId) cancelAnimationFrame(this.state.animationFrameId);
if(this.state.blinkIntervalId) clearInterval(this.state.blinkIntervalId);
this.state.animationFrameId = null;
this.state.blinkIntervalId = null;
}
resize() {
this.destroyAnimation();
this.init(); // 重新绘制空网格
// 这里依赖dashboard.js在resize后重新获取并传入数据
}
debounce(func, delay) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), delay);
};
}
}