// 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); }; } }