用Python+HTML做的点名器

更新日志

0.2.2

0.2.1

  1. 使用AES-ECB加密本地名单文件,防止有些同学乱改

0.1.2

  1. 修复末尾数取不到的问题
  2. 将学号模式设为默认

0.1.1

请先配置环境

python3.8
建议使用conda(下面的操作均在conda中进行)

conda create -n lottery python=3.8 -y
conda activate lottery
pip install pandas datetime sip pyqt5 pyqt5-tools PyQtWebEngine openpyxl pywin32 xlrd pycryptodome

上代码

控制台 console.py

# -*- coding: utf-8 -*-
import sys
import os
import utils
from win32api import ShellExecute
from subprocess import Popen
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QFileDialog, QLabel
from PyQt5.QtCore import QCoreApplication, QRect
from PyQt5.QtGui import QIcon, QFont


# 创建主窗口
class MainWindow(QMainWindow):
    btn = {}
	
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setupUi()
	
    def setupUi(self):
        self.setObjectName("mainWindow")  # 设置窗口名
        self.setWindowTitle('抽号工具 控制台')  # 设置标题
        self.setWindowIcon(QIcon('icons/console.png'))  # 设置图标
        self.resize(800, 600)  # 重置大小
        # 引入字体
        self.font = QFont()
        self.font.setFamily("霞鹜文楷")
        self.font.setPointSize(14)
        self.font.setKerning(True)
		
        # 构建窗口内控件
        self.title = self.label('抽号工具 控制台', (300, 10, 200, 40))
		
        self.btn['load'] = self.button('导入', self.load, (150, 80, 100, 40))
        self.load_tip = self.label('请导入标准花名册', (270, 80, 200, 40))
        self.btn['clear'] = self.button('清除', self.clear, (150, 150, 100, 40))
        self.clear_tip = self.label('清除所有列表', (270, 150, 200, 40))
        self.btn['start'] = self.button('启动', self.start, (150, 220, 100, 40))
        self.start_tip = self.label('启动抽号器', (270, 220, 500, 40))
        self.cprt = self.label(
            '<html><head/><body><p align="center">©2020-2021 sakuyark.com 版权所有</p></body></html>', (0, 550, 800, 40))
        self.cprt.setMouseTracking(True)
        self.explain = self.label(
            QCoreApplication.translate(
                "mainWindow",
                u"""<html><head/><body><p>本抽号器由 hanjin@Sakuyark 制作</p><p>Github: <a href="https://github.com/syhanjin/lottery"><span style="text-decoration: underline; color:#0000ff;">https://github.com/syhanjin/lottery</span></a></p><p><span style="font-weight:600;">因为比较懒,数据方面比较简洁</span></p></body></html>""",
                None
            ),
            (150, 350, 500, 200)
        )
	
    def label(
        self,
        text: 'str',
        geometry: 'tuple[int, int, int, int]',
    ):
        """
        构建label控件
        :param text: 标签文本
        :param geometry: x, y, width, height
        """
        label = QLabel(self)
        if hasattr(self, 'font'):
            label.setFont(self.font)
        label.setText(text)
        label.setGeometry(QRect(*geometry))
        return label
	
    def button(
        self,
        text: 'str',
        action: 'function',
        geometry: 'tuple[int, int, int, int]'
    ) -> QPushButton:
        """
        构建button控件
        :param text: 按钮文本
        :param action: 点击事件
        :param geometry: x, y, width, height
        """
        btn = QPushButton(text, self)
        if hasattr(self, 'font'):
            btn.setFont(self.font)
        btn.setGeometry(QRect(*geometry))
        btn.clicked.connect(action)
        return btn
	
    def load(self):
        """
        导入名单,名单内至少包含两列 姓名 学号
        """
        self.load_tip.setText('准备导入...')
        try:
            fileName, fileType = QFileDialog.getOpenFileName(
                self,
                "导入", os.getcwd(), "excel(*.xls; *.xlsx; *.xlsm; *.xlsb)"
            )
            self.load_tip.setText('导入中...')
            utils.data.load(fileName)
            self.load_tip.setText('导入成功!')
        except Exception as e:
            print(e)
            self.load_tip.setText('导入失败!')
	
    def start(self):
        """
        启动抽号器主程序 main.exe or main.py
        """
        self.start_tip.setText('启动中...')
        try:
            if (os.path.exists('main.exe')):
                ShellExecute(0, 'open', 'main.exe', '', '', 1)
            else:
                Popen(
                    'conda activate lottery&&python main.py', shell=True)
            self.start_tip.setText('启动成功!请等待窗口弹出...')
        except Exception as e:
            print(e)
            self.start_tip.setText('启动失败!')
	
    def clear(self):
        """
        清除已经导入的名单
        """
        self.clear_tip.setText('正在清除所有已导入的列表...')
        try:
            os.remove(utils.data.listjs)
            self.clear_tip.setText('清除成功!')
        except Exception as e:
            print(e)
            self.clear_tip.setText('清除失败!')


if __name__ == '__main__':
    app = QApplication(sys.argv)
    main = MainWindow()
    main.show()
    # 运行应用,并监听事件
    sys.exit(app.exec_())

主程序 main.py

# -*- coding: utf-8 -*-

import sys
from utils import prpc
from PyQt5.QtWidgets import QMainWindow, QApplication
from PyQt5.QtCore import QUrl, QFileInfo
from PyQt5.QtGui import QIcon
from PyQt5.QtWebEngineWidgets import QWebEngineView


# 创建主窗口
class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 重置大小
        self.resize(800, 600)
        # 设置标题
        self.setWindowTitle('抽号工具')
        # 设置图标
        self.setWindowIcon(QIcon('icons/lottery-win.png'))
        # 浏览器空间
        self.webview = QWebEngineView()
        # 指定页面
        self.webview.load(
            QUrl(QFileInfo('./html/main.html').absoluteFilePath()))
        self.setCentralWidget(self.webview)
        # 加载完成后后台对前端进行一些处理
        self.webview.loadFinished.connect(self.call_js)
    
    def call_js(self):
        # 解码加密的名单
        self.webview.page().runJavaScript(
            f'list_decrypt("{prpc.key.decode("utf-8")}")'
        )
        # 将页面设置为 学号模式
        self.webview.page().runJavaScript(
            'change_tab("number", false)'
        )


# 程序入口
if __name__ == "__main__":
    app = QApplication(sys.argv)
    # 创建主窗口
    browser = MainWindow()
    browser.show()
    # 运行应用,并监听事件
    sys.exit(app.exec_())

utils.__init__.py

from Crypto.Cipher import AES
from binascii import b2a_hex, a2b_hex


class PrpCrypt(object):
    """
    AES 加密类
    """
	
    def __init__(self, key):
        self.key = key.encode('utf-8')
        self.mode = AES.MODE_ECB
	
	
    # 加密函数,如果text不足16位就用空格补足为16位,
    # 如果大于16当时不是16的倍数,那就补足为16的倍数。
    def add_to_16(self, text):
        if len(text.encode('utf-8')) % 16:
            add = 16 - (len(text.encode('utf-8')) % 16)
        else:
            add = 0
        text = text + ('\0' * add)
        return text.encode('utf-8')
	
    def encrypt(self, text):
        cryptor = AES.new(
            self.key,
            self.mode,
        )
        # 这里密钥key 长度必须为16(AES-128),
        # 24(AES-192),或者32 (AES-256)Bytes 长度
        # 目前AES-128 足够目前使用
        text = self.add_to_16(text)
        self.ciphertext = cryptor.encrypt(text)
        # 因为AES加密时候得到的字符串不一定是ascii字符集的,输出到终端或者保存时候可能存在问题
        # 所以这里统一把加密后的字符串转化为16进制字符串
        # print(self.ciphertext)
        return b2a_hex(self.ciphertext)
	
    # 解密后,去掉补足的空格用strip() 去掉
    def decrypt(self, text):
        cryptor = AES.new(self.key, self.mode)
        plain_text = cryptor.decrypt(a2b_hex(text))
        # return plain_text.rstrip('\0')
        return bytes.decode(plain_text).rstrip('\0')


# ECB 加密方式
# 秘钥 16(AES-128) / 24(AES-192) / 32(AES-256) Bytes 长度
prpc = PrpCrypt('==lottery-2021==')
#               ^................^

from . import data

utils.data.py

from pandas import read_excel
import os
from . import prpc

listroot = os.path.join('.', 'html', 'lists')
listjs = os.path.join(listroot, 'list.js')


def load(fp: str):
    # 读取名单
    df = read_excel(fp)
    # 提取学号后两位数字
    df['学号'] = df['学号'].str[-2:]
    # 构建名单文件夹
    if not os.path.exists(listroot):
        os.makedirs(listroot)
    # 构建名单文件
    if not os.path.exists(listjs):
        with open(listjs, 'w') as f:
            f.write('window.name_lists = {}\n')
    # 构建json
    l = '''{\n  "data": [\n'''
    for i, row in df.iterrows():
        l += f'''    {{"number": "{row["学号"]}", "name": "{row["姓名"]}"}},\n'''
    l = l[:-2] + '\n'
    l += '  ]\n}\n'
    # AES-ECB 加密
    l = prpc.encrypt(l).decode('utf-8')
    # 输出到文件
    with open(
        os.path.join(
            listjs
        ), 'a+', encoding='utf-8'
    ) as f:
        fn = fp.rsplit('/', 1)[1]
        f.write(f'''\nwindow.name_lists["{fn}"] = `{l}`''')

html/main.html

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <meta name="viewport"
        content="width=device-width,initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>抽号工具</title>
    <link rel='stylesheet' href='./css/main.css'>
    <script src='./js/jquery/jquery-3.4.0.min.js'></script>
    <script src='./js/CryptoJS/rollups/aes.js'></script>
    <script src='./js/CryptoJS/components/cipher-core-min.js'></script>
    <script src='./js/CryptoJS/components/aes-min.js'></script>
    <script src='./js/CryptoJS/components/core-min.js'></script>
    <script src='./js/CryptoJS/components/mode-ecb-min.js'></script>
    <script src='./js/CryptoJS/components/pad-zeropadding-min.js'></script>
    <script id="lists" src='./lists/list.js'></script>
    <script src='./js/main.js'></script>
</head>

<body>
    <div class="main">
        <div class="tabs">
            <input type="button" value="姓名模式" class="tab select" id="name">
            <input type="button" value="学号模式" class="tab" id="number">
        </div>
        <ul class="contents">
            <li class="tab-box name">
                <div>
                    <div class="settings">
                        <select></select>
                    </div>
                    <p class="tip">等待后台解密...</p>
                    <div class="roll">
                        <span>未开始</span>
                    </div>
                    <div class="control">
                        <input type="button" value="开始">
                    </div>
                </div>
            </li>
            <li class="tab-box number">
                <div>
                    <div class="settings">
                        <input type="number" value="1" min="1" class="start" />
                        <span>~</span>
                        <input type="number" value="52" max="99" class="end" />
                    </div>
                    <div class="roll">
                        <span>0</span>
                        <span>0</span>
                    </div>
                    <div class="control">
                        <input type="button" value="开始">
                    </div>
                </div>
            </li>
        </ul>
        <p class="kidding">因个人审美问题,有点丑;因懒的问题,动画勉强能看</p>
        <p class="cprt">©2020-2021 sakuyark.com 版权所有</p>
    </div>
</body>

</html>

html/css/main.css

@font-face {
    font-family: '霞鹜文楷';
    src: url(../fonts/LXGWWenKai-Regular.ttf);
}

html {
    font-size: 62.5%;
    height: 100%;
    width: 100%;
}


html,
body {
    height: 100%;
}

* {
    margin: 0;
    padding: 0;
    font-family: '霞鹜文楷';
    box-sizing: border-box;
}

.main {
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

.tabs {
    height: 5vh;
    line-height: 5vh;
    display: flex;
    align-items: center;
}



.tabs *:last-child {
    margin-right: 0;
}

.contents {
    height: 100%;
    flex: 1;
    font-size: 2.2rem;
    white-space: nowrap;
    -webkit-perspective: 1000;
    -webkit-backface-visibility: hidden;
    -webkit-tap-highlight-color: transparent;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
    left: 0;
    list-style-type: none;
    overflow: hidden;
    -webkit-perspective: 1000;
    -webkit-backface-visibility: hidden;
    transform: translate3d(0, 0, 0);
    touch-action: pan-y;
    user-select: none;
    -webkit-user-drag: none;
    -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
    width: 200vw;
}

.contents>* {
    display: block;
    height: 100%;
}

.contents>*>div {
    display: flex;
    flex-direction: column;
    height: 100%;
}

.tab-box {
    padding: 2rem;
    display: inline-block;
    width: 100vw;
    height: 100%;
    touch-action: pan-y;
    user-select: none;
    -webkit-user-drag: none;
    -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
    list-style-type: none;
    float: left;
    color: #000;
    text-align: center;
}
}

.tab-box.select {
    /* display: block; */
}

.tab:hover:not(.select) {
    -webkit-filter: brightness(1.3);
    -moz-filter: brightness(1.3);
    -o-filter: brightness(1.3);
    -ms-filter: brightness(1.3);
}

.tab {
    flex: 1;
    border: 0;
    outline: none;
    height: 100%;
    font-size: 2.2rem;
    margin-right: 0.2em;
    background-color: skyblue;
}

.tab.select {
    background-color: rgb(190, 190, 190);
}

.number {}

.settings {
    margin: 0.5rem 0;
    text-align: center;
    display: flex;
    align-items: center;
    justify-content: center;
    height: 5vh;
    line-height: 5vh;
    font-size: 4vh;
}

.number .settings input {
    width: 10vh;
    font-size: 4vh;
    margin: 0 2vh;
    text-align: center;
    height: 100%;
    outline: 0;
    border: 0;
    background-color: skyblue;
}

.number .settings input[disabled] {
    background-color: rgb(187, 187, 187);
}

.roll {
    display: flex;
    flex: 1;
    padding: 1rem 5rem;
    align-items: start;
    justify-content: center;

    -moz-user-select: none;
    /*火狐*/
    -webkit-user-select: none;
    /*webkit浏览器*/
    -ms-user-select: none;
    /*IE10*/
    -khtml-user-select: none;
    /*早期浏览器*/
    user-select: none;
}

@keyframes beat {
    from {
        transform: translateY(-.5rem)
    }

    to {
        transform: translateY(.5rem);
    }
}

.roll span {
    margin: 0 5rem;
    text-align: center;
    line-height: 50vh;
}

.number .roll span {
    font-size: 50vh;
}

.name .roll span {
    font-size: 30vh;
}

.roll span.select {
    animation: beat 0.1s 0s linear infinite;
}

.control {
    flex: 0.5;
    width: 100%;
    text-align: center;
}

.control input {
    outline: 0;
    border: 0;
    width: 40%;
    height: 50%;
    font-size: 6rem;
    letter-spacing: .5em;
    font-weight: bold;
    text-align: center;
}


.kidding {
    display: block;
    position: absolute;
    bottom: 10vh;
    margin: 0 auto;
    left: 0;
    right: 0;
    text-align: center;
    font-size: 2vw;
}

.cprt {
    position: absolute;
    bottom: 0;
    text-align: center;
    left: 0;
    right: 0;
    font-size: 3vh;
}

.name .settings select {
    width: 25vw;
    height: 4vw;
    font-size: 2vw;
    text-align: center;
    text-align-last: center;
    border: 0;
    outline: 0;
    background: skyblue;
}
.name .settings select option {
    width: 25vw;
    height: 4vw;
    font-size: 2vw;
    text-align: center;
    text-align-last: center;
    border: 0;
    outline: 0;
    background: skyblue;
}

/*

@keyframes turn {
    0% {
        -webkit-transform: rotate(0deg);
    }

    25% {
        -webkit-transform: rotate(90deg);
    }

    50% {
        -webkit-transform: rotate(180deg);
    }

    75% {
        -webkit-transform: rotate(270deg);
    }

    100% {
        -webkit-transform: rotate(360deg);
    }
}

#reload.select {
    animation: turn 1s linear infinite;
    transform-origin: 52% 52%;
}
*/ 

.tip {
    font-size: 2vw;
    height: 2vw;
    line-height: 2vw;
    color: gray;
}

html/js/main.js

// AES-ECB 解密
function decrypt(data, key) {
    var key1 = CryptoJS.enc.Utf8.parse(key);
    var encryptedHexStr = CryptoJS.enc.Hex.parse(data);
    var encryptedBase64Str = CryptoJS.enc.Base64.stringify(encryptedHexStr);
    var decrypted = CryptoJS.AES.decrypt(encryptedBase64Str, key1, {
        mode: CryptoJS.mode.ECB,
        padding: CryptoJS.pad.ZeroPadding
    });
    return decrypted.toString(CryptoJS.enc.Utf8);
}

interval = 0;

// 修改模式
function change_tab(id, animation = true) {
    $('.tab.select').removeClass('select');
    $('#' + id).addClass('select');
    e = $('li.' + id);
    transformx = e.index() * e.outerWidth(true);
    if (animation) {
        $('.main .contents').animate({
            'top': '-' + transformx + 'px'
        }, {
            step: function (now, fx) {
                $(this).css({
                    "transform": "translate3d(" + now + "px, 0px, 0px)",
                });
            }
        }, 1000);
    } else {
        $('.main .contents').css({
            "transform": "translate3d(-" + transformx + "px, 0px, 0px)",
            'top': '-' + transformx + 'px'
        });
    }
}

// 随机数
function randint(st, ed) {
    return st + parseInt(Math.random() * (ed - st + 1));
}


// number
number_state = 0, st = 0, ed = 0, s1 = null, s2 = null;

function number_start() {
    number_state = 1;
    $('.number .settings input[type=number]').attr('disabled', 'disabled');
    $('.number .control input').val('停止');
    $('.roll span').addClass('select')
    st = parseInt($('input[type=number].start').val());
    ed = parseInt($('input[type=number].end').val());
    ten = [parseInt(st / 10), parseInt(ed / 10)]
    interval = setInterval(function () {
        number = randint(st, ed);
        s1.text(parseInt(number / 10));
        s2.text(number % 10);
    }, 1000 / 12)
}

function number_stop() {
    number_state = 0;
    $('.number .settings input[type=number]').removeAttr('disabled')
    $('.number .control input').val('开始');
    clearInterval(interval)
    number = randint(st, ed);
    s1.text(parseInt(number / 10));
    s2.text(number % 10);
    $('.roll span').removeClass('select')
}

function number_toggle() {
    if (number_state == 0) {
        number_start();
    } else if (number_state == 1) {
        number_stop();
    }
}

// --


// name

// 解密名单
function list_decrypt(key) {
    if (window.name_lists == undefined || window.name_lists.length == 0) return;

    $('.name .tip').text('正在解密名单...');
    try {
        for (i in window.name_lists) {
            decr = decrypt(window.name_lists[i], key);
            window.name_lists[i] = JSON.parse(decr).data;
        }
    } catch {
        $('.name .tip').text('名单解密失败!请重启程序再试!');
    }
    $('.name .tip').text('名单解密成功!');
    setTimeout(function () {
        $('.name .tip').animate({
            'opacity': 0
        }, 1500, function () {
            $(this).hide();
        });
    });
}

name_state = 0, xlsx = '', s = null;

function name_start() {
    name_state = 1;
    $('.name .settings select').attr('disabled', 'disabled')
    $('.name .control input').val('停止');
    $('.roll span').addClass('select')
    xlsx = $('.name .settings select').val();
    interval = setInterval(function () {
        s.text(
            window.name_lists[xlsx][randint(0, window.name_lists[xlsx].length - 1)]['name']
        )
    }, 1000 / 12)
}

function name_stop() {
    name_state = 0;
    $('.name .settings select').removeAttr('disabled')
    $('.name .control input').val('开始');
    clearInterval(interval);
    s.text(
        window.name_lists[xlsx][randint(0, window.name_lists[xlsx].length - 1)]['name']
    );
    $('.roll span').removeClass('select')
}

function name_toggle() {
    if (name_state == 0) {
        name_start();
    } else if (name_state == 1) {
        name_stop();
    }
}

// 加载名单
function load_lists() {
    $('.name .settings select').empty();
    if (window.name_lists == undefined || window.name_lists.length == 0) {
        $('.name .tip').text('未找到名单');
        $('.name .control input').attr('disabled', 'disabled');
        $('.name .settings select').attr('disabled', 'disabled');
    }
    for (i in window.name_lists) {
        option = document.createElement('option');
        option.value = i;
        option.innerText = i;
        $('.name .settings select').append(option);
    }
}


$(document).ready(function () {
    s1 = $('.number .roll span:first-child');
    s2 = $('.number .roll span:last-child');
    s = $('.name .roll span');
    $('.tab').on('click', function (e) {
        change_tab(e.target.id)
        clearInterval(interval);
        $('.roll span').removeClass('select');
    });
    $('.number .control input').on('click', function () {
        number_toggle()
    });
    $('.name .control input').on('click', function () {
        name_toggle()
    });
    $('.name .settings select').on('change', function () {
        s.text('未开始');
    });
    load_lists();
    $('#reload').on('click', function (e) {
        $(this).addClass('select');
        window.location.reload();
        load_lists();
        $(this).removeClass('select');
    });
});

项目地址

Github

安装包 v0.2.2

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值