JavaScript秒表跨页面控制:管理员页操控,客户端页显示时间
Hey there! I’ve built similar real-time stopwatch systems for local sports tournaments, so let’s walk through exactly how to implement your setup. Here’s a step-by-step breakdown that’s tested and ready to adapt:
We’ll use WebSocket-based real-time communication (via Socket.IO) to sync the stopwatch state instantly between the admin panel and client displays—critical for low-latency sports event use. We’ll also integrate MySQL for fetching event details, with a Node.js/Express backend handling both the real-time sync and database queries.
1. Backend Setup (Node.js + Socket.IO + MySQL)
First, initialize your project and install dependencies:
npm init -y && npm install express socket.io mysql2
Then create your server file (server.js) with the following code—this handles Socket connections, stopwatch state management, and MySQL data retrieval:
const express = require('express'); const http = require('http'); const { Server } = require('socket.io'); const mysql = require('mysql2/promise'); const app = express(); const server = http.createServer(app); const io = new Server(server, { cors: { origin: ["https://your-admin-domain.com", "https://your-client-display-domain.com"] } // Lock this down in production! }); // Global stopwatch state (persist to Redis/MySQL if you need server restart resilience) let stopwatchState = { isRunning: false, startTime: null, elapsed: 0 }; // MySQL connection pool (efficient for repeated queries) const pool = mysql.createPool({ host: 'your-mysql-host', user: 'your-db-user', password: 'your-db-password', database: 'sports_events_db' }); // Handle Socket connections io.on('connection', (socket) => { // Send current stopwatch state to new connections socket.emit('update-stopwatch', stopwatchState); // Admin triggers start socket.on('start-stopwatch', () => { if (!stopwatchState.isRunning) { stopwatchState.isRunning = true; stopwatchState.startTime = Date.now() - stopwatchState.elapsed; io.emit('update-stopwatch', stopwatchState); // Broadcast to all clients } }); // Admin triggers stop socket.on('stop-stopwatch', () => { if (stopwatchState.isRunning) { stopwatchState.isRunning = false; stopwatchState.elapsed = Date.now() - stopwatchState.startTime; io.emit('update-stopwatch', stopwatchState); } }); // Admin triggers reset socket.on('reset-stopwatch', () => { stopwatchState = { isRunning: false, startTime: null, elapsed: 0 }; io.emit('update-stopwatch', stopwatchState); }); }); // API endpoint to fetch MySQL event data app.get('/api/events', async (req, res) => { try { const [rows] = await pool.query('SELECT * FROM event_details ORDER BY event_time ASC'); res.json(rows); } catch (err) { console.error('MySQL query error:', err); res.status(500).json({ error: 'Failed to load event data' }); } }); server.listen(3000, () => console.log('Backend running on port 3000'));
2. Admin Control Panel (Simple HTML/JS)
This page is for staff only—no stopwatch display, just the three control buttons:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Stopwatch Admin Control</title> <script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script> <style> .control-btn { padding: 15px 30px; margin: 0 10px; font-size: 24px; border: none; border-radius: 8px; cursor: pointer; } .start { background: #2ecc71; color: white; } .stop { background: #e74c3c; color: white; } .reset { background: #95a5a6; color: white; } body { display: flex; flex-direction: column; align-items: center; margin-top: 100px; font-family: Arial, sans-serif; } </style> </head> <body> <h1>Event Stopwatch Controls</h1> <div class="btn-group"> <button class="control-btn start" onclick="triggerStart()">Start</button> <button class="control-btn stop" onclick="triggerStop()">Stop</button> <button class="control-btn reset" onclick="triggerReset()">Reset</button> </div> <script> const socket = io('https://your-backend-domain.com'); function triggerStart() { socket.emit('start-stopwatch'); } function triggerStop() { socket.emit('stop-stopwatch'); } function triggerReset() { socket.emit('reset-stopwatch'); } </script> </body> </html>
3. Client Display (Big Screen Ready)
This page is for the TV大屏—only shows the stopwatch and MySQL event data, no controls:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Sports Event Stopwatch</title> <script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script> <style> body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #000; color: white; } .stopwatch { font-size: 12vw; font-weight: bold; text-align: center; margin-top: 5vh; } .events-section { max-width: 90vw; margin: 10vh auto 0; } .events-section h2 { font-size: 4vw; text-align: center; } .event-item { padding: 2vw; border-bottom: 1px solid #333; font-size: 2.5vw; } </style> </head> <body> <div class="stopwatch" id="stopwatchDisplay">00:00:00</div> <div class="events-section"> <h2>Upcoming Events</h2> <div id="eventsList"></div> </div> <script> const socket = io('https://your-backend-domain.com'); const stopwatchDisplay = document.getElementById('stopwatchDisplay'); const eventsList = document.getElementById('eventsList'); let updateInterval; // Format milliseconds to HH:MM:SS function formatElapsedTime(elapsedMs) { const totalSeconds = Math.floor(elapsedMs / 1000); const hours = String(Math.floor(totalSeconds / 3600)).padStart(2, '0'); const minutes = String(Math.floor((totalSeconds % 3600) / 60)).padStart(2, '0'); const seconds = String(totalSeconds % 60).padStart(2, '0'); return `${hours}:${minutes}:${seconds}`; } // Update stopwatch display based on state socket.on('update-stopwatch', (state) => { // Clear existing interval to avoid duplicates if (updateInterval) clearInterval(updateInterval); if (state.isRunning) { // Update every second while running updateInterval = setInterval(() => { const currentElapsed = Date.now() - state.startTime; stopwatchDisplay.textContent = formatElapsedTime(currentElapsed); }, 1000); } else { // Show static elapsed time when stopped stopwatchDisplay.textContent = formatElapsedTime(state.elapsed); } }); // Fetch and display MySQL event data async function loadEventData() { try { const response = await fetch('https://your-backend-domain.com/api/events'); const events = await response.json(); eventsList.innerHTML = events.map(event => ` <div class="event-item"> <h3>${event.event_name}</h3> <p>Time: ${new Date(event.event_time).toLocaleString()}</p> <p>Venue: ${event.venue}</p> </div> `).join(''); } catch (err) { eventsList.innerHTML = '<p>Failed to load event data</p>'; } } // Load events on page load window.addEventListener('load', loadEventData); </script> </body> </html>
Key Production Tips
- CORS Security: Never use
origin: "*"in production—specify only your admin and client domains to prevent unauthorized access. - State Persistence: If your backend restarts, the stopwatch state will reset. Add Redis or a MySQL table to save the state so it can be restored on server startup.
- 大屏适配: Use
vw(viewport width) units for text to ensure it scales perfectly on any TV size. - MySQL Optimization: Add indexes to your
event_detailstable (e.g., onevent_time) to speed up queries when dealing with large datasets. - Authentication: Add password protection to the admin panel so only authorized staff can control the stopwatch—you can use simple HTTP basic auth or a full OAuth flow.
内容的提问来源于stack exchange,提问作者Venom791

