做PPT演示的时候来个现场抽奖送书

1,043 阅读4分钟

之前有机会做技术分享,但怕内容枯燥乏味,于是就想在PPT上做一些花样,就有了下面的一些想法。

  • 作为程序员,尤其是前端开发,自然不能用传统的PPT,网页PPT是不是更花哨一点,比如 slidevreveal.js,考虑简便选了后者
  • 弄一些特效什么的,各种滑动动画,背景渐变等,这些作为前端开发相信不难,比如弄上粒子背景动画,这里有粒子库particles.js
  • 最重要的是讲完一部分,最好弄个抽奖什么的,和年会一样;于是就想到自己有几本前端开发的闲书,自己看过没有啥用了,索性送给大家

前面两个比较好弄,都有现成的模板,官方也有例子,很快就弄好了内容

现状和思考

下面重点想怎么弄现场抽奖,现状考虑:

  • 参会的人很多不认识,也不确定是哪些,所以无法预先把名字确定
  • 有远程通过直播听的,也要考虑在内
  • 现在一般都有手机,能否弄个现场扫码,填姓名参与抽奖?

附带一些问题:

  • 现场人可能乱填,需要有删除功能,或者批量清空
  • 如果有人没有带手机,可以由主持手动添加
  • 全部人都填完了,核验完再开始,需要开始和停止按钮
  • 允许多次抽奖,需要重置按钮

image.png

粗略的做了一下大概就是上面的界面了,其中遇见几个重点问题和解决:

服务端

reveal.js仅提供PPT的一些效果和内容展示,都是静态的,所以需要起一个本地的server来支持扫码请求的数据收集

最简单的就是用Koa了,丰富的插件和中间件,这里只需要一个能支持静态文件目录的koa-static-resolver中间件,外加一个路由管理@koa/router,方便处理请求即可

const Koa = require('koa');
const KSR = require('koa-static-resolver');
const KoaRouter = require('@koa/router');

const app = new Koa();

app.use(KSR({
    dirs: ['./']
}));

const router = new KoaRouter();
router.all('/:action', (ctx) => {
    //...
});
app.use(router.routes()).use(router.allowedMethods());

二维码生成

浏览器端有现成的库qrcode直接通过canvas生成二维码

const $qrcodeCanvas = document.querySelector('.lucky-qrcode-canvas');
window.QRCode.toCanvas($qrcodeCanvas, url, {
    width: 300
}, function(error) {
    if (error) {
        console.error(error);
    }
    console.log('qrcode success!');
});

现在大多主流手机app都有扫码功能,然后二维码内容指向一个url自动跳转即可;但现场扫码,跳转的url有这些问题:

  • 如果url能提供公网域名的,那肯定没问题,但成本太高,为了一次分享弄个公网域名的url麻烦
  • 前面不是启动了本地server,自然考虑直接连到演示的机器上,现场情况基本大家都是用的公司Wifi,也就是在一个局域网内,或者vpn的环境,也能连内网ip,所以需要把演示机器的内网IP给找到
const getInternalIp = () => {
    const n = os.networkInterfaces();
    //console.log(n);
    const list = [];
    for (const k in n) {
        const inter = n[k];
        for (const j in inter) {
            const item = inter[j];
            if (item.family === 'IPv4' && !item.internal) {
                const a = item.address;
                console.log(`Internal IP: ${a}`);
                if (a.startsWith('192.') || a.startsWith('10.')) {
                    list.push(a);
                }
            }
        }
    }
    return list.pop();
};

这里直接用的nodejs的os.networkInterfaces接口,过滤出来192.或10.开头的ip即可

输入页面

这里直接用bootstrap的样式,做了个最简单表单输入提交即可

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>抽奖</title>
    <link href="../../node_modules/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
</head>

<body style="padding: 10%;">

    <div class="mb-3">
        <label for="exampleFormControlInput1" class="form-label">
            <h5>姓名:</h5>
        </label>
        <input type="text" class="form-control name-value" id="exampleFormControlInput1" />
    </div>
    <div>
        <button type="button" class="btn btn-primary name-submit">提交</button>
    </div>
    <div class="alert alert-success" style="margin-top: 20px;display: none;" role="alert">
        添加成功
    </div>
    <div class="alert alert-danger" style="margin-top: 20px;display: none;" role="alert">
        提交失败
      </div>
    <script>
        const $name = document.querySelector('.name-value');
        const $submit = document.querySelector('.name-submit');
        const $danger = document.querySelector('.alert-danger');
        const $success = document.querySelector('.alert-success');

        $submit.addEventListener('click', function(e) {

            $danger.style.display = 'none';
            $success.style.display = 'none';
            const v = $name.value;

            if (!v) {
                $name.focus();
                return;
            }
            $name.setAttribute('disabled', 'disabled');
            $submit.setAttribute('disabled', 'disabled');

            console.log(v);
            window.fetch(`/add?name=${v}`).then(function(response) {
                return response.json();
            }, function() {
                $danger.style.display = 'block';
                $name.removeAttribute('disabled');
                $submit.removeAttribute('disabled');
            }).then(function(data) {
                console.log(data);
                if (!data) {
                    return;
                }
                $success.style.display = 'block';
                $submit.style.display = 'none';
            });

        });

    </script>

</body>
 </html>

image.png

名称管理和渲染

用户提交了名称,可以直接存内存,也不需要什么数据库,直接用一个自动去重的Set即可

const cacheList = new Set();
//增
cacheList.add(name);
//删
cacheList.delete(name);
//清空
cacheList.clear();

每次更新需要通知到浏览器端,可以使用websocket实时刷新,这里用的成熟的库socket.io,自带服务端和客户端,服务端也支持嵌入各种流行的web server框架,比如这里使用的Koa框架

const SocketIO = require('socket.io');
const app = new Koa();
const server = http.createServer(app.callback());
const socketIO = SocketIO(server, {
    maxHttpBufferSize: 1e8
});
socketIO.on('connection', function(client) {
    client.on('data', function(data) {
        
    });
    client.on('disconnect', function() {
        
    });
    //new user connected
});
const sockets = socketIO.sockets;

//发送名称列表
const nameList = Array.from(cacheList);
nameList.sort();
    
sockets.emit('data', {
    action: 'list',
    data: nameList
});

这样只要cacheList更新,通过sockets.emit方法就可以把最新数据发送到浏览器端,下面看浏览器端怎么接收:

  • 首先是要在浏览器页面引入socket.io的客户端,在socket.io/client-dist目录下
<script src="node_modules/socket.io/client-dist/socket.io.min.js"></script>
  • 然后初始化客户端接收器
const initSocket = function() {

    const showLog = function(msg) {
        console.log(`[socket] ${msg}`);
    };

    if (!window.io) {
        showLog('not found io');
        return;
    }

    const socket = window.io.connect('/');

    lucky.socket = socket;

    let server_connected = false;
    let has_error = false;
    let reconnect_times = 0;

    const reload = function() {
        socket.close();
        window.location.reload();
    };

    socket.on('data', function(data) {
        if (server_connected) {
            if (data.action === 'list' && data.data) {
                initList(data.data);
            }
        }
    });
    socket.on('connect', function(data) {
        showLog('socket connected');
        if (server_connected) {
            if (has_error) {
                reload();
            }
        }
        server_connected = true;
        has_error = false;
        reconnect_times = 0;
    });

    socket.on('connect_error', function(data) {
        showLog('socket connection error');
        has_error = true;
    });

    socket.on('connect_timeout', function(data) {
        showLog('socket connection timeout');
    });

    socket.on('reconnecting', function(data) {
        reconnect_times += 1;
        showLog(`socket reconnecting ... ${reconnect_times}`);
        if (reconnect_times > 20) {
            socket.close();
            showLog(`socket closed after retry ${reconnect_times} times`);
        }
    });

    socket.on('reconnect_error', function(data) {
        showLog('socket reconnection error');
        has_error = true;
    });

    socket.on('reconnect_failed', function(data) {
        showLog('socket reconnection failed');
        has_error = true;
    });

};

如果顺利,服务端和浏览器就实时连接起来了,可以实时相互传输数据了

抽取动画和随机抽取结果

  • 动画其实这里用了一个流行缓动类tween.js
<script src="node_modules/@tweenjs/tween.js/dist/tween.umd.js"></script>

一开始有考虑过three.js做成3D的,但时间有限,大家可以试试

  • 然后就是随机抽取结果,图简单就是直接math random到索引,然后最终就是索引所在的人中奖,将动画最终定位到这里即可
const index = Math.floor(lucky.list.length * Math.random());

完整的代码

git clone lucky-turntable (github.com)