当前位置: 首页 > news >正文

前端实现扫描大屏幕二维码上传图片并同步显示方案

文章目录

    • 1. 项目概述与实现思路
      • 技术栈
      • 实现流程图
    • 2. 系统架构设计
      • 2.1 整体架构
      • 2.2 核心模块
    • 3. 详细实现步骤
      • 3.1 二维码生成模块
      • 3.2 图片上传模块
      • 3.3 WebSocket实时通信模块
        • 服务器端(Node.js + WebSocket)
        • 大屏幕WebSocket客户端
      • 3.4 大屏幕Canvas展示模块
      • 3.5 完整的大屏幕页面代码
    • 4. 部署与优化
      • 4.1 服务器部署
      • 4.2 项目目录结构
      • 4.3 运行项目
    • 5. 功能扩展与优化建议
      • 5.1 功能扩展
      • 5.2 性能优化
    • 6. 总结

1. 项目概述与实现思路

本文将详细介绍如何通过前端技术实现扫描大屏幕上的二维码,上传图片并实时同步显示在大屏幕上的完整方案。该方案适用于会议、展览、互动活动等场景,让用户可以通过手机扫描二维码上传图片,大屏幕实时展示用户上传的内容。

技术栈

  • 前端:HTML5 + CSS3 + JavaScript
  • 二维码生成:QRCode.js
  • 图片上传:原生JavaScript + FormData
  • 实时通信:WebSocket
  • 大屏显示:Canvas绘图

实现流程图

用户打开手机相机
扫描大屏幕二维码
跳转至图片上传页面
选择/拍摄图片
上传图片至服务器
服务器处理图片
通过WebSocket推送至大屏幕
大屏幕Canvas渲染图片
显示特效动画

2. 系统架构设计

2.1 整体架构

用户手机端(上传端) <--> 服务器 <--> 大屏幕显示端

2.2 核心模块

  1. 二维码生成模块 - 在大屏幕生成包含上传地址的二维码
  2. 图片上传模块 - 手机端实现图片选择和上传
  3. 实时通信模块 - 使用WebSocket实现图片数据实时推送
  4. 大屏展示模块 - 使用Canvas渲染图片和动画效果

3. 详细实现步骤

3.1 二维码生成模块

首先,我们需要在大屏幕上显示一个包含图片上传页面URL的二维码。

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>大屏幕图片展示系统</title><script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.0/build/qrcode.min.js"></script><style>body {margin: 0;padding: 0;background: #000;color: #fff;font-family: Arial, sans-serif;overflow: hidden;}#container {position: relative;width: 100vw;height: 100vh;}#qrcode-container {position: absolute;top: 20px;right: 20px;background: white;padding: 10px;border-radius: 8px;z-index: 100;}#qrcode-container p {color: #000;text-align: center;margin: 5px 0;font-size: 14px;}#display-area {width: 100%;height: 100%;position: relative;}#canvas {position: absolute;top: 0;left: 0;width: 100%;height: 100%;}</style>
</head>
<body><div id="container"><div id="qrcode-container"><p>扫描二维码上传图片</p><div id="qrcode"></div></div><div id="display-area"><canvas id="canvas"></canvas></div></div><script>// 生成二维码function generateQRCode() {// 获取当前页面的URL,替换为上传页面地址const uploadPageUrl = window.location.origin + '/upload.html';// 生成二维码QRCode.toCanvas(document.getElementById('qrcode'), uploadPageUrl, {width: 150,height: 150,colorDark: "#000000",colorLight: "#ffffff",correctLevel: QRCode.CorrectLevel.H}, function(error) {if (error) console.error(error);console.log('二维码生成成功!');});}// 页面加载完成后生成二维码window.addEventListener('load', generateQRCode);</script>
</body>
</html>

3.2 图片上传模块

创建手机端上传页面(upload.html),用户扫描二维码后将访问此页面。

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>上传图片到大屏幕</title><style>* {margin: 0;padding: 0;box-sizing: border-box;}body {font-family: 'Helvetica Neue', Arial, sans-serif;background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);color: #333;line-height: 1.6;min-height: 100vh;padding: 20px;display: flex;flex-direction: column;align-items: center;justify-content: center;}.container {max-width: 500px;width: 100%;background: white;border-radius: 20px;box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);padding: 30px;text-align: center;}h1 {margin-bottom: 20px;color: #333;font-size: 24px;}.upload-area {border: 2px dashed #667eea;border-radius: 15px;padding: 40px 20px;margin: 20px 0;transition: all 0.3s ease;cursor: pointer;position: relative;}.upload-area:hover {background-color: #f8f9ff;border-color: #764ba2;}.upload-area i {font-size: 48px;color: #667eea;margin-bottom: 15px;display: block;}.upload-area p {margin: 10px 0;color: #666;}#file-input {display: none;}.preview-container {margin: 20px 0;display: none;}#preview {max-width: 100%;max-height: 300px;border-radius: 10px;box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);}.btn {background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);color: white;border: none;padding: 12px 30px;border-radius: 50px;font-size: 16px;cursor: pointer;transition: transform 0.3s ease, box-shadow 0.3s ease;margin: 10px 5px;box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);}.btn:hover {transform: translateY(-3px);box-shadow: 0 8px 20px rgba(102, 126, 234, 0.6);}.btn:active {transform: translateY(0);}.btn:disabled {background: #cccccc;cursor: not-allowed;transform: none;box-shadow: none;}.progress-container {width: 100%;height: 8px;background: #f0f0f0;border-radius: 4px;margin: 20px 0;overflow: hidden;display: none;}.progress-bar {height: 100%;background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);width: 0%;transition: width 0.3s ease;}.message {margin-top: 15px;padding: 10px;border-radius: 5px;display: none;}.success {background: #d4edda;color: #155724;border: 1px solid #c3e6cb;}.error {background: #f8d7da;color: #721c24;border: 1px solid #f5c6cb;}.camera-btn {background: #28a745;margin-top: 10px;}</style>
</head>
<body><div class="container"><h1>上传图片到大屏幕</h1><div class="upload-area" id="upload-area"><i>📷</i><p>点击选择图片或直接拍照</p><p class="hint">支持 JPG、PNG 格式,大小不超过 5MB</p><input type="file" id="file-input" accept="image/*"><button class="btn camera-btn" id="camera-btn">打开相机拍照</button></div><div class="preview-container" id="preview-container"><img id="preview" src="" alt="图片预览"></div><div class="progress-container" id="progress-container"><div class="progress-bar" id="progress-bar"></div></div><button class="btn" id="upload-btn" disabled>上传图片</button><button class="btn" id="reset-btn">重新选择</button><div class="message" id="message"></div></div><script>// DOM元素const fileInput = document.getElementById('file-input');const uploadArea = document.getElementById('upload-area');const previewContainer = document.getElementById('preview-container');const preview = document.getElementById('preview');const uploadBtn = document.getElementById('upload-btn');const resetBtn = document.getElementById('reset-btn');const progressContainer = document.getElementById('progress-container');const progressBar = document.getElementById('progress-bar');const message = document.getElementById('message');const cameraBtn = document.getElementById('camera-btn');// 当前选择的文件let currentFile = null;// 点击上传区域触发文件选择uploadArea.addEventListener('click', () => {fileInput.click();});// 文件选择变化事件fileInput.addEventListener('change', handleFileSelect);// 处理文件选择function handleFileSelect(event) {const file = event.target.files[0];if (file) {validateAndPreviewFile(file);}}// 验证并预览文件function validateAndPreviewFile(file) {// 验证文件类型if (!file.type.match('image/jpeg') && !file.type.match('image/png')) {showMessage('请选择JPG或PNG格式的图片!', 'error');return;}// 验证文件大小 (5MB)if (file.size > 5 * 1024 * 1024) {showMessage('图片大小不能超过5MB!', 'error');return;}currentFile = file;// 预览图片const reader = new FileReader();reader.onload = function(e) {preview.src = e.target.result;previewContainer.style.display = 'block';uploadBtn.disabled = false;};reader.readAsDataURL(file);}// 上传图片uploadBtn.addEventListener('click', uploadImage);function uploadImage() {if (!currentFile) return;// 显示进度条progressContainer.style.display = 'block';uploadBtn.disabled = true;// 创建FormData对象const formData = new FormData();formData.append('image', currentFile);formData.append('timestamp', new Date().getTime());// 创建XMLHttpRequest对象const xhr = new XMLHttpRequest();// 上传进度事件xhr.upload.addEventListener('progress', function(e) {if (e.lengthComputable) {const percentComplete = (e.loaded / e.total) * 100;progressBar.style.width = percentComplete + '%';}});// 请求完成事件xhr.addEventListener('load', function() {if (xhr.status === 200) {showMessage('图片上传成功! 已显示在大屏幕上。', 'success');setTimeout(() => {resetUploader();}, 3000);} else {showMessage('上传失败,请重试!', 'error');uploadBtn.disabled = false;}progressContainer.style.display = 'none';});// 请求错误事件xhr.addEventListener('error', function() {showMessage('上传出错,请检查网络连接!', 'error');progressContainer.style.display = 'none';uploadBtn.disabled = false;});// 发送请求xhr.open('POST', '/api/upload');xhr.send(formData);}// 重置上传器resetBtn.addEventListener('click', resetUploader);function resetUploader() {fileInput.value = '';previewContainer.style.display = 'none';uploadBtn.disabled = true;progressContainer.style.display = 'none';message.style.display = 'none';currentFile = null;}// 显示消息function showMessage(text, type) {message.textContent = text;message.className = 'message ' + type;message.style.display = 'block';}// 相机功能cameraBtn.addEventListener('click', openCamera);function openCamera() {if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {showMessage('您的设备不支持相机功能!', 'error');return;}// 请求相机权限navigator.mediaDevices.getUserMedia({ video: true }).then(function(stream) {// 创建相机预览界面createCameraInterface(stream);}).catch(function(error) {console.error('相机访问错误:', error);showMessage('无法访问相机,请确保已授予相机权限!', 'error');});}function createCameraInterface(stream) {// 创建相机界面const cameraModal = document.createElement('div');cameraModal.style.cssText = `position: fixed;top: 0;left: 0;width: 100%;height: 100%;background: rgba(0,0,0,0.9);z-index: 1000;display: flex;flex-direction: column;align-items: center;justify-content: center;`;const video = document.createElement('video');video.srcObject = stream;video.autoplay = true;video.style.cssText = `max-width: 90%;max-height: 70%;border-radius: 10px;`;const captureBtn = document.createElement('button');captureBtn.textContent = '拍照';captureBtn.className = 'btn';captureBtn.style.cssText = `margin-top: 20px;background: #28a745;`;const closeBtn = document.createElement('button');closeBtn.textContent = '关闭';closeBtn.className = 'btn';closeBtn.style.cssText = `margin-top: 10px;background: #dc3545;`;cameraModal.appendChild(video);cameraModal.appendChild(captureBtn);cameraModal.appendChild(closeBtn);document.body.appendChild(cameraModal);// 拍照功能captureBtn.addEventListener('click', function() {const canvas = document.createElement('canvas');const context = canvas.getContext('2d');canvas.width = video.videoWidth;canvas.height = video.videoHeight;context.drawImage(video, 0, 0, canvas.width, canvas.height);// 将canvas转换为Blobcanvas.toBlob(function(blob) {// 停止视频流stream.getTracks().forEach(track => track.stop());// 移除相机界面document.body.removeChild(cameraModal);// 处理拍摄的图片const file = new File([blob], 'camera_capture.jpg', { type: 'image/jpeg' });validateAndPreviewFile(file);}, 'image/jpeg', 0.9);});// 关闭相机closeBtn.addEventListener('click', function() {stream.getTracks().forEach(track => track.stop());document.body.removeChild(cameraModal);});}</script>
</body>
</html>

3.3 WebSocket实时通信模块

为了实现大屏幕实时显示上传的图片,我们需要使用WebSocket建立实时通信。

服务器端(Node.js + WebSocket)
// server.js
const WebSocket = require('ws');
const http = require('http');
const express = require('express');
const path = require('path');
const multer = require('multer');const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });// 存储所有连接的客户端(大屏幕)
const screens = new Set();// 配置multer用于文件上传
const storage = multer.diskStorage({destination: function (req, file, cb) {cb(null, 'uploads/')},filename: function (req, file, cb) {cb(null, Date.now() + '-' + file.originalname)}
});const upload = multer({ storage: storage,limits: {fileSize: 5 * 1024 * 1024 // 5MB限制},fileFilter: function (req, file, cb) {// 只允许图片文件if (file.mimetype.startsWith('image/')) {cb(null, true);} else {cb(new Error('只允许上传图片文件!'), false);}}
});// 静态文件服务
app.use(express.static(path.join(__dirname, 'public')));
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));// 文件上传接口
app.post('/api/upload', upload.single('image'), (req, res) => {if (!req.file) {return res.status(400).json({ error: '没有上传文件!' });}// 构建图片URLconst imageUrl = `/uploads/${req.file.filename}`;// 向所有大屏幕客户端发送新图片const message = JSON.stringify({type: 'newImage',imageUrl: imageUrl,timestamp: new Date().toISOString()});screens.forEach(client => {if (client.readyState === WebSocket.OPEN) {client.send(message);}});res.json({ success: true, message: '图片上传成功!',imageUrl: imageUrl});
});// WebSocket连接处理
wss.on('connection', (ws, req) => {console.log('新的客户端连接');// 检查是否是大屏幕客户端(通过查询参数识别)const url = new URL(req.url, `http://${req.headers.host}`);if (url.searchParams.get('type') === 'screen') {screens.add(ws);console.log('大屏幕客户端已连接');// 发送连接成功消息ws.send(JSON.stringify({type: 'connected',message: '已成功连接到服务器'}));}// 连接关闭处理ws.on('close', () => {screens.delete(ws);console.log('客户端断开连接');});// 错误处理ws.on('error', (error) => {console.error('WebSocket错误:', error);screens.delete(ws);});
});const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {console.log(`服务器运行在端口 ${PORT}`);
});
大屏幕WebSocket客户端

在大屏幕页面中添加WebSocket客户端代码:

// 在大屏幕页面中添加WebSocket连接
let ws;
let reconnectAttempts = 0;
const maxReconnectAttempts = 5;function connectWebSocket() {// 构建WebSocket URL,添加type=screen标识为大屏幕客户端const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';const wsUrl = `${protocol}//${window.location.host}?type=screen`;ws = new WebSocket(wsUrl);ws.onopen = function() {console.log('已连接到服务器');reconnectAttempts = 0;};ws.onmessage = function(event) {const data = JSON.parse(event.data);console.log('收到消息:', data);if (data.type === 'newImage') {// 收到新图片,显示在大屏幕上displayNewImage(data.imageUrl);}};ws.onclose = function() {console.log('连接已关闭');// 尝试重新连接if (reconnectAttempts < maxReconnectAttempts) {reconnectAttempts++;console.log(`尝试重新连接... (${reconnectAttempts}/${maxReconnectAttempts})`);setTimeout(connectWebSocket, 3000);}};ws.onerror = function(error) {console.error('WebSocket错误:', error);};
}// 初始化WebSocket连接
connectWebSocket();

3.4 大屏幕Canvas展示模块

在大屏幕页面中添加Canvas展示功能:

// 图片显示管理
class ImageDisplayManager {constructor(canvas) {this.canvas = canvas;this.ctx = canvas.getContext('2d');this.images = [];this.currentIndex = 0;this.animationState = 0; // 0: 显示中, 1: 淡出, 2: 切换, 3: 淡入this.animationProgress = 0;this.animationSpeed = 0.02;// 设置Canvas尺寸this.resizeCanvas();window.addEventListener('resize', () => this.resizeCanvas());// 开始动画循环this.animate();}resizeCanvas() {this.canvas.width = window.innerWidth;this.canvas.height = window.innerHeight;}addImage(url) {const img = new Image();img.onload = () => {this.images.push(img);// 如果当前没有图片在显示,立即显示新图片if (this.images.length === 1) {this.currentIndex = 0;this.animationState = 3; // 开始淡入动画this.animationProgress = 0;}};img.src = url;}animate() {// 清空画布this.ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);if (this.images.length > 0) {const currentImage = this.images[this.currentIndex];// 根据动画状态处理显示switch (this.animationState) {case 0: // 正常显示this.drawImage(currentImage, 1);// 显示一段时间后开始淡出if (this.animationProgress >= 1) {this.animationState = 1; // 开始淡出this.animationProgress = 0;} else {this.animationProgress += 0.001;}break;case 1: // 淡出this.drawImage(currentImage, 1 - this.animationProgress);this.animationProgress += this.animationSpeed;if (this.animationProgress >= 1) {this.animationState = 2; // 切换到下一张this.animationProgress = 0;}break;case 2: // 切换图片this.currentIndex = (this.currentIndex + 1) % this.images.length;this.animationState = 3; // 开始淡入break;case 3: // 淡入this.drawImage(currentImage, this.animationProgress);this.animationProgress += this.animationSpeed;if (this.animationProgress >= 1) {this.animationState = 0; // 正常显示this.animationProgress = 0;}break;}}requestAnimationFrame(() => this.animate());}drawImage(img, opacity) {// 计算图片显示尺寸,保持宽高比const canvasRatio = this.canvas.width / this.canvas.height;const imgRatio = img.width / img.height;let drawWidth, drawHeight, offsetX, offsetY;if (imgRatio > canvasRatio) {// 图片比画布宽drawWidth = this.canvas.width;drawHeight = this.canvas.width / imgRatio;offsetX = 0;offsetY = (this.canvas.height - drawHeight) / 2;} else {// 图片比画布高drawHeight = this.canvas.height;drawWidth = this.canvas.height * imgRatio;offsetX = (this.canvas.width - drawWidth) / 2;offsetY = 0;}// 设置透明度this.ctx.globalAlpha = opacity;// 绘制图片this.ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);// 重置透明度this.ctx.globalAlpha = 1;// 添加一些视觉效果this.addVisualEffects(opacity);}addVisualEffects(opacity) {// 添加光晕效果const gradient = this.ctx.createRadialGradient(this.canvas.width / 2, this.canvas.height / 2, 0,this.canvas.width / 2, this.canvas.height / 2, this.canvas.width * 0.7);gradient.addColorStop(0, `rgba(255, 255, 255, ${0.1 * opacity})`);gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');this.ctx.fillStyle = gradient;this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);}
}// 初始化图片显示管理器
const canvas = document.getElementById('canvas');
const imageManager = new ImageDisplayManager(canvas);// 显示新图片的函数
function displayNewImage(imageUrl) {imageManager.addImage(imageUrl);
}

3.5 完整的大屏幕页面代码

整合所有功能的大屏幕完整代码:

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>大屏幕图片展示系统</title><script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.0/build/qrcode.min.js"></script><style>body {margin: 0;padding: 0;background: #000;color: #fff;font-family: Arial, sans-serif;overflow: hidden;}#container {position: relative;width: 100vw;height: 100vh;}#qrcode-container {position: absolute;top: 20px;right: 20px;background: white;padding: 10px;border-radius: 8px;z-index: 100;box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);}#qrcode-container p {color: #000;text-align: center;margin: 5px 0;font-size: 14px;font-weight: bold;}#display-area {width: 100%;height: 100%;position: relative;}#canvas {position: absolute;top: 0;left: 0;width: 100%;height: 100%;}#status {position: absolute;bottom: 20px;left: 20px;background: rgba(0, 0, 0, 0.7);padding: 10px 15px;border-radius: 5px;font-size: 14px;z-index: 100;}.connected {color: #4CAF50;}.disconnected {color: #F44336;}</style>
</head>
<body><div id="container"><div id="qrcode-container"><p>扫描二维码上传图片</p><div id="qrcode"></div></div><div id="display-area"><canvas id="canvas"></canvas></div><div id="status">连接状态: <span id="status-text">连接中...</span></div></div><script>// 生成二维码function generateQRCode() {// 获取当前页面的URL,替换为上传页面地址const uploadPageUrl = window.location.origin + '/upload.html';// 生成二维码QRCode.toCanvas(document.getElementById('qrcode'), uploadPageUrl, {width: 150,height: 150,colorDark: "#000000",colorLight: "#ffffff",correctLevel: QRCode.CorrectLevel.H}, function(error) {if (error) console.error(error);console.log('二维码生成成功!');});}// 图片显示管理类class ImageDisplayManager {constructor(canvas) {this.canvas = canvas;this.ctx = canvas.getContext('2d');this.images = [];this.currentIndex = 0;this.animationState = 0; // 0: 显示中, 1: 淡出, 2: 切换, 3: 淡入this.animationProgress = 0;this.animationSpeed = 0.02;// 设置Canvas尺寸this.resizeCanvas();window.addEventListener('resize', () => this.resizeCanvas());// 开始动画循环this.animate();}resizeCanvas() {this.canvas.width = window.innerWidth;this.canvas.height = window.innerHeight;}addImage(url) {const img = new Image();img.onload = () => {this.images.push(img);// 如果当前没有图片在显示,立即显示新图片if (this.images.length === 1) {this.currentIndex = 0;this.animationState = 3; // 开始淡入动画this.animationProgress = 0;}};img.src = url;}animate() {// 清空画布this.ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);if (this.images.length > 0) {const currentImage = this.images[this.currentIndex];// 根据动画状态处理显示switch (this.animationState) {case 0: // 正常显示this.drawImage(currentImage, 1);// 显示一段时间后开始淡出if (this.animationProgress >= 1) {this.animationState = 1; // 开始淡出this.animationProgress = 0;} else {this.animationProgress += 0.001;}break;case 1: // 淡出this.drawImage(currentImage, 1 - this.animationProgress);this.animationProgress += this.animationSpeed;if (this.animationProgress >= 1) {this.animationState = 2; // 切换到下一张this.animationProgress = 0;}break;case 2: // 切换图片this.currentIndex = (this.currentIndex + 1) % this.images.length;this.animationState = 3; // 开始淡入break;case 3: // 淡入this.drawImage(currentImage, this.animationProgress);this.animationProgress += this.animationSpeed;if (this.animationProgress >= 1) {this.animationState = 0; // 正常显示this.animationProgress = 0;}break;}}requestAnimationFrame(() => this.animate());}drawImage(img, opacity) {// 计算图片显示尺寸,保持宽高比const canvasRatio = this.canvas.width / this.canvas.height;const imgRatio = img.width / img.height;let drawWidth, drawHeight, offsetX, offsetY;if (imgRatio > canvasRatio) {// 图片比画布宽drawWidth = this.canvas.width;drawHeight = this.canvas.width / imgRatio;offsetX = 0;offsetY = (this.canvas.height - drawHeight) / 2;} else {// 图片比画布高drawHeight = this.canvas.height;drawWidth = this.canvas.height * imgRatio;offsetX = (this.canvas.width - drawWidth) / 2;offsetY = 0;}// 设置透明度this.ctx.globalAlpha = opacity;// 绘制图片this.ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);// 重置透明度this.ctx.globalAlpha = 1;// 添加一些视觉效果this.addVisualEffects(opacity);}addVisualEffects(opacity) {// 添加光晕效果const gradient = this.ctx.createRadialGradient(this.canvas.width / 2, this.canvas.height / 2, 0,this.canvas.width / 2, this.canvas.height / 2, this.canvas.width * 0.7);gradient.addColorStop(0, `rgba(255, 255, 255, ${0.1 * opacity})`);gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');this.ctx.fillStyle = gradient;this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);}}// WebSocket连接管理let ws;let reconnectAttempts = 0;const maxReconnectAttempts = 5;const statusText = document.getElementById('status-text');function connectWebSocket() {// 构建WebSocket URL,添加type=screen标识为大屏幕客户端const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';const wsUrl = `${protocol}//${window.location.host}?type=screen`;ws = new WebSocket(wsUrl);ws.onopen = function() {console.log('已连接到服务器');statusText.textContent = '已连接';statusText.className = 'connected';reconnectAttempts = 0;};ws.onmessage = function(event) {const data = JSON.parse(event.data);console.log('收到消息:', data);if (data.type === 'newImage') {// 收到新图片,显示在大屏幕上displayNewImage(data.imageUrl);} else if (data.type === 'connected') {console.log(data.message);}};ws.onclose = function() {console.log('连接已关闭');statusText.textContent = '连接断开';statusText.className = 'disconnected';// 尝试重新连接if (reconnectAttempts < maxReconnectAttempts) {reconnectAttempts++;console.log(`尝试重新连接... (${reconnectAttempts}/${maxReconnectAttempts})`);statusText.textContent = `重新连接中... (${reconnectAttempts}/${maxReconnectAttempts})`;setTimeout(connectWebSocket, 3000);}};ws.onerror = function(error) {console.error('WebSocket错误:', error);statusText.textContent = '连接错误';statusText.className = 'disconnected';};}// 初始化图片显示管理器const canvas = document.getElementById('canvas');const imageManager = new ImageDisplayManager(canvas);// 显示新图片的函数function displayNewImage(imageUrl) {// 添加服务器基础URLconst fullImageUrl = window.location.origin + imageUrl;imageManager.addImage(fullImageUrl);// 显示通知showNotification('新图片已上传!');}// 显示通知function showNotification(message) {const notification = document.createElement('div');notification.textContent = message;notification.style.cssText = `position: fixed;top: 20px;left: 50%;transform: translateX(-50%);background: rgba(0, 0, 0, 0.8);color: white;padding: 10px 20px;border-radius: 5px;z-index: 1000;font-size: 16px;transition: opacity 0.5s;`;document.body.appendChild(notification);// 3秒后淡出并移除setTimeout(() => {notification.style.opacity = '0';setTimeout(() => {if (notification.parentNode) {document.body.removeChild(notification);}}, 500);}, 3000);}// 页面加载完成后初始化window.addEventListener('load', function() {generateQRCode();connectWebSocket();});</script>
</body>
</html>

4. 部署与优化

4.1 服务器部署

创建package.json文件:

{"name": "screen-image-upload","version": "1.0.0","description": "大屏幕图片上传展示系统","main": "server.js","scripts": {"start": "node server.js","dev": "nodemon server.js"},"dependencies": {"express": "^4.18.2","ws": "^8.13.0","multer": "^1.4.5-lts.1"},"devDependencies": {"nodemon": "^2.0.22"}
}

4.2 项目目录结构

project/
├── server.js
├── package.json
├── uploads/
│   └── (上传的图片文件)
└── public/├── index.html (大屏幕页面)└── upload.html (上传页面)

4.3 运行项目

  1. 安装依赖:
npm install
  1. 启动服务器:
npm start
  1. 访问大屏幕页面:
http://localhost:3000
  1. 手机扫描二维码访问上传页面:
http://localhost:3000/upload.html

5. 功能扩展与优化建议

5.1 功能扩展

  1. 图片审核功能:添加后台审核机制,防止不当内容显示
  2. 图片滤镜:上传前添加滤镜和编辑功能
  3. 多屏同步:支持多个大屏幕同步显示
  4. 数据统计:统计上传数量、热门图片等数据
  5. 社交分享:允许用户分享自己上传的图片

5.2 性能优化

  1. 图片压缩:服务器端自动压缩大图片
  2. CDN加速:使用CDN分发图片资源
  3. 负载均衡:多服务器部署应对高并发
  4. 缓存策略:合理设置缓存提高响应速度

6. 总结

本文详细介绍了如何通过前端技术实现扫描大屏幕二维码上传图片并实时同步显示的完整方案。该系统结合了二维码生成、图片上传、WebSocket实时通信和Canvas动画展示等技术,提供了一个完整的互动解决方案。

该方案具有以下特点:

  • 用户体验良好:手机端操作简单,大屏幕展示效果炫酷
  • 实时性强:通过WebSocket实现图片即时推送
  • 扩展性好:模块化设计便于功能扩展
  • 兼容性强:支持多种设备和浏览器

在这里插入图片描述

http://www.dtcms.com/a/618665.html

相关文章:

  • 免费域名网站wordpress 修改子主题
  • 二叉树的前序遍历
  • 网站规划建设与管理维护课后答案四平市城乡建设局网站
  • 『大模型部署』NVIDIA Orin + bnb量化 + Qwen3-VL | 4bit、8bit量化
  • 网站建设状况vue快速搭建网站
  • 深入数据库性能优化:从参数调优到RAC高可用架构构建
  • MaxTex下载及LaTex环境配置
  • HttpServletResponse 详细指南
  • 网站建设3a模型是什么意思即墨网站建设哪家好
  • 为什么网站设计很少全屏网络维护难吗
  • 北京做网站的公司商集客电话专业app开发设计的公司
  • SpringCache缓存
  • kubernetes基于sealos工具快速安装指导
  • Linux 信号机制
  • SpringBoot19-HttpClient 详解及 SpringBoot 使用指南
  • 17做网店一样的网站网站按域名跳转不同的页面
  • 13.2 国产之光崛起:深度求索与通义千问的技术突破
  • 旅游网站建设的结论阿里云商标注册官网
  • 第五次:郑州银行杯2025郑州马拉松
  • Three.js使用教程
  • Reqable 工具报错 Netbare Code Error Unknown
  • 宝山网页设计制作黄石seo诊断
  • git-Git约定式提交
  • wap建站教程0元玩手游平台
  • nw.js桌面软件开发系列 第.节 HTML和桌面软件开发的碰撞
  • 设计一套网站费用北京网页
  • 7.3、Python-函数的返回值
  • 网站建设咨询话术技巧网站开发程序设计
  • 【Qt】配置安卓开发环境
  • 基于Qt,调用千问7B大模型,实现智能对话