欢迎光临
我们一直在努力

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

文章目录

    • 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绘图

实现流程图

#mermaid-svg-vQcCgxkZ1WXFDRHk {font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-vQcCgxkZ1WXFDRHk .error-icon{fill:#552222;}#mermaid-svg-vQcCgxkZ1WXFDRHk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vQcCgxkZ1WXFDRHk .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-vQcCgxkZ1WXFDRHk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vQcCgxkZ1WXFDRHk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vQcCgxkZ1WXFDRHk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vQcCgxkZ1WXFDRHk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vQcCgxkZ1WXFDRHk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vQcCgxkZ1WXFDRHk .marker.cross{stroke:#333333;}#mermaid-svg-vQcCgxkZ1WXFDRHk svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vQcCgxkZ1WXFDRHk .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-vQcCgxkZ1WXFDRHk .cluster-label text{fill:#333;}#mermaid-svg-vQcCgxkZ1WXFDRHk .cluster-label span{color:#333;}#mermaid-svg-vQcCgxkZ1WXFDRHk .label text,#mermaid-svg-vQcCgxkZ1WXFDRHk span{fill:#333;color:#333;}#mermaid-svg-vQcCgxkZ1WXFDRHk .node rect,#mermaid-svg-vQcCgxkZ1WXFDRHk .node circle,#mermaid-svg-vQcCgxkZ1WXFDRHk .node ellipse,#mermaid-svg-vQcCgxkZ1WXFDRHk .node polygon,#mermaid-svg-vQcCgxkZ1WXFDRHk .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vQcCgxkZ1WXFDRHk .node .label{text-align:center;}#mermaid-svg-vQcCgxkZ1WXFDRHk .node.clickable{cursor:pointer;}#mermaid-svg-vQcCgxkZ1WXFDRHk .arrowheadPath{fill:#333333;}#mermaid-svg-vQcCgxkZ1WXFDRHk .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vQcCgxkZ1WXFDRHk .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vQcCgxkZ1WXFDRHk .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-vQcCgxkZ1WXFDRHk .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-vQcCgxkZ1WXFDRHk .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vQcCgxkZ1WXFDRHk .cluster text{fill:#333;}#mermaid-svg-vQcCgxkZ1WXFDRHk .cluster span{color:#333;}#mermaid-svg-vQcCgxkZ1WXFDRHk div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vQcCgxkZ1WXFDRHk :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

用户打开手机相机

扫描大屏幕二维码

跳转至图片上传页面

选择/拍摄图片

上传图片至服务器

服务器处理图片

通过WebSocket推送至大屏幕

大屏幕Canvas渲染图片

显示特效动画

2. 系统架构设计

2.1 整体架构

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

2.2 核心模块

  • 二维码生成模块 – 在大屏幕生成包含上传地址的二维码
  • 图片上传模块 – 手机端实现图片选择和上传
  • 实时通信模块 – 使用WebSocket实现图片数据实时推送
  • 大屏展示模块 – 使用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转换为Blob
    canvas.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: '没有上传文件!' });
    }

    // 构建图片URL
    const 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) {
    // 添加服务器基础URL
    const 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 运行项目

  • 安装依赖:
  • npm install

  • 启动服务器:
  • npm start

  • 访问大屏幕页面:
  • http://localhost:3000

  • 手机扫描二维码访问上传页面:
  • http://localhost:3000/upload.html

    5. 功能扩展与优化建议

    5.1 功能扩展

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

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

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

    该方案具有以下特点:

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

    在这里插入图片描述

    赞(0)
    未经允许不得转载:171主机测评 » 前端实现扫描大屏幕二维码上传图片并同步显示方案
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址