
目录结构
python/
├── sql/
│ └── table.sql # 创建数据库及数据表
├── config/
│ └── __init__.py # 数据库和Flask配置
├── static/
│ ├── style.css # 样式文件
│ └── script.js # JavaScript脚本
├── templates/
│ └── index.html # 主页面模板
└── lucky_draw.py # 主应用程序
1.table.sql
table.sql
CREATE DATABASE lucky_draw;
USE lucky_draw;CREATE TABLE participants (id INT AUTO_INCREMENT PRIMARY KEY,name VARCHAR(50) NOT NULL,department VARCHAR(50),employee_id VARCHAR(20),join_time DATETIME DEFAULT CURRENT_TIMESTAMP
);CREATE TABLE winners (id INT AUTO_INCREMENT PRIMARY KEY,participant_id INT,draw_time DATETIME DEFAULT CURRENT_TIMESTAMP,FOREIGN KEY (participant_id) REFERENCES participants(id)
);
2.__init__.py
DB_CONFIG = {'host': 'localhost','user': 'your_username','password': 'your_password','database': 'lucky_draw'
}
3.style.css
body {font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;margin: 0;padding: 20px;background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);min-height: 100vh;
}.container {max-width: 1000px;margin: 0 auto;padding: 30px;background-color: white;border-radius: 15px;box-shadow: 0 5px 20px rgba(0,0,0,0.1);
}header {text-align: center;margin-bottom: 40px;
}h1 {color: #2c3e50;font-size: 2.5em;margin-bottom: 10px;
}.subtitle {color: #7f8c8d;font-size: 1.2em;margin: 0;
}h2 {color: #34495e;font-size: 1.5em;margin-bottom: 20px;
}.section {margin: 30px 0;padding: 25px;border-radius: 10px;background-color: #f8f9fa;box-shadow: 0 2px 10px rgba(0,0,0,0.05);
}.draw-section {background: linear-gradient(to right, #fff5f5, #fff0f0);
}.form-inline {display: flex;justify-content: center;
}.input-group {display: flex;align-items: center;gap: 10px;
}.input-group input {padding: 12px;border: 2px solid #e0e0e0;border-radius: 8px;font-size: 1em;transition: all 0.3s ease;
}.input-group input:focus {border-color: #3498db;outline: none;box-shadow: 0 0 5px rgba(52,152,219,0.3);
}.draw-controls {display: flex;flex-wrap: wrap;gap: 20px;align-items: center;justify-content: center;
}.checkbox-group {display: flex;align-items: center;gap: 5px;
}.btn {padding: 12px 24px;border: none;border-radius: 8px;cursor: pointer;font-size: 1em;transition: all 0.3s ease;display: flex;align-items: center;gap: 8px;
}.btn-primary {background-color: #3498db;color: white;
}.btn-primary:hover {background-color: #2980b9;transform: translateY(-2px);
}.btn-danger {background-color: #e74c3c;color: white;
}.btn-danger:hover {background-color: #c0392b;
}.reset-form {text-align: center;margin-top: 20px;
}.dashboard {display: grid;grid-template-columns: 1fr;gap: 30px;margin-top: 40px;
}.name-list {display: flex;flex-wrap: wrap;gap: 12px;padding: 15px;background-color: white;border-radius: 8px;min-height: 50px;
}.name-tag {padding: 8px 16px;background-color: #f0f2f5;border-radius: 20px;font-size: 0.9em;display: flex;align-items: center;gap: 8px;transition: all 0.3s ease;
}.name-tag:hover {transform: translateY(-2px);
}.winner {background: linear-gradient(45deg, #ffd700, #ffa500);color: #000;box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}.alert {padding: 15px 20px;margin: 20px 0;border-radius: 8px;background-color: #d4edda;color: #155724;text-align: center;display: flex;align-items: center;justify-content: center;gap: 10px;animation: slideIn 0.5s ease;
}.count {font-size: 0.8em;color: #666;font-weight: normal;
}footer {text-align: center;margin-top: 40px;color: #7f8c8d;
}@keyframes slideIn {from {transform: translateY(-20px);opacity: 0;}to {transform: translateY(0);opacity: 1;}
}@media (max-width: 768px) {.container {padding: 20px;}.draw-controls {flex-direction: column;}.input-group {width: 100%;}
}
[data-theme="dark"] {background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
}[data-theme="dark"] .container {background-color: #2c3e50;color: #ecf0f1;
}[data-theme="dark"] .section {background-color: #34495e;
}[data-theme="dark"] .draw-section {background: linear-gradient(to right, #2c3e50, #34495e);
}[data-theme="dark"] h1,
[data-theme="dark"] h2 {color: #ecf0f1;
}[data-theme="dark"] .name-tag {background-color: #465c74;color: #ecf0f1;
}
.theme-switch {position: fixed;top: 20px;right: 20px;display: flex;align-items: center;gap: 10px;
}.switch {position: relative;display: inline-block;width: 60px;height: 34px;
}.switch input {opacity: 0;width: 0;height: 0;
}.slider {position: absolute;cursor: pointer;top: 0;left: 0;right: 0;bottom: 0;background-color: #ccc;transition: .4s;
}.slider:before {position: absolute;content: "";height: 26px;width: 26px;left: 4px;bottom: 4px;background-color: white;transition: .4s;
}input:checked + .slider {background-color: #2196F3;
}input:checked + .slider:before {transform: translateX(26px);
}.slider.round {border-radius: 34px;
}.slider.round:before {border-radius: 50%;
}
.lottery-animation {margin: 20px 0;padding: 20px;text-align: center;
}.lottery-box {position: relative;overflow: hidden;display: inline-block;padding: 30px 60px;background: linear-gradient(45deg, #f1c40f, #f39c12);border-radius: 15px;box-shadow: 0 4px 15px rgba(0,0,0,0.2);animation: pulse 1.5s infinite;
}.rolling-name-text {font-size: 2.5em;color: #fff;text-shadow: 2px 2px 4px rgba(0,0,0,0.3);margin-bottom: 10px;
}.rolling-dept-text {font-size: 1.2em;color: rgba(255, 255, 255, 0.9);text-shadow: 1px 1px 2px rgba(0,0,0,0.2);
}@keyframes pulse {0% {transform: scale(1);box-shadow: 0 4px 15px rgba(0,0,0,0.2);}50% {transform: scale(1.05);box-shadow: 0 8px 25px rgba(0,0,0,0.3);}100% {transform: scale(1);box-shadow: 0 4px 15px rgba(0,0,0,0.2);}
}
.winners-section .name-tag {font-size: 1.2em;padding: 10px 20px;background: linear-gradient(45deg, #ffd700, #ffa500);box-shadow: 0 2px 8px rgba(0,0,0,0.1);transition: all 0.3s ease;
}.winners-section .name-tag:hover {transform: translateY(-3px) rotate(3deg);box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.charts-container {display: flex;justify-content: center;margin-top: 20px;
}.chart-wrapper {width: 300px;height: 300px;
}
@media (max-width: 768px) {.chart-wrapper {width: 100%;height: auto;}
}
.winner-highlight {animation: winner-glow 1s ease-in-out infinite alternate;transform: scale(1.1);transition: all 0.3s ease;
}@keyframes winner-glow {from {box-shadow: 0 0 10px #fff, 0 0 20px #fff, 0 0 30px #e60073, 0 0 40px #e60073;}to {box-shadow: 0 0 20px #fff, 0 0 30px #ff4da6, 0 0 40px #ff4da6, 0 0 50px #ff4da6;}
}
.celebration {position: absolute;top: 0;left: 0;right: 0;bottom: 0;display: flex;justify-content: center;align-items: center;background: rgba(0, 0, 0, 0.8);animation: fadeIn 0.3s ease-out;border-radius: 15px;
}.celebration-content {text-align: center;animation: popIn 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}.celebration-icon {font-size: 4em;margin-bottom: 10px;animation: bounce 1s infinite;
}.winner-name {font-size: 2.5em;color: #fff;text-shadow: 2px 2px 4px rgba(0,0,0,0.5);margin-bottom: 5px;
}.winner-dept {font-size: 1.2em;color: rgba(255, 255, 255, 0.9);
}@keyframes fadeIn {from { opacity: 0; }to { opacity: 1; }
}@keyframes popIn {0% {transform: scale(0.3);opacity: 0;}100% {transform: scale(1);opacity: 1;}
}@keyframes bounce {0%, 100% {transform: translateY(0);}50% {transform: translateY(-20px);}
}
4.script.js
const themeToggle = document.getElementById('theme-toggle');
const html = document.documentElement;themeToggle.addEventListener('change', () => {if (themeToggle.checked) {html.setAttribute('data-theme', 'dark');localStorage.setItem('theme', 'dark');} else {html.setAttribute('data-theme', 'light');localStorage.setItem('theme', 'light');}
});
const savedTheme = localStorage.getItem('theme') || 'light';
html.setAttribute('data-theme', savedTheme);
themeToggle.checked = savedTheme === 'dark';
const startDrawBtn = document.getElementById('start-draw');
const drawForm = document.getElementById('draw-form');
const rollingName = document.getElementById('rolling-name');
const rollingSound = document.getElementById('rolling-sound');
const winnerSound = document.getElementById('winner-sound');let isRolling = false;function getAvailableParticipants() {const excludeWinners = document.querySelector('input[name="exclude_winners"]').checked;if (excludeWinners) {return participants.filter(p => !p.is_winner);}return participants;
}function showParticipant(participant) {rollingName.innerHTML = `<div class="rolling-name-text">${participant.name}</div><div class="rolling-dept-text">${participant.department}</div>`;
}function getRandomParticipant(available) {const randomIndex = Math.floor(Math.random() * available.length);return available[randomIndex];
}
function getRandomWinners(available, count) {const shuffled = [...available].sort(() => 0.5 - Math.random());return shuffled.slice(0, Math.min(count, available.length));
}startDrawBtn.addEventListener('click', async () => {if (isRolling) return;const available = getAvailableParticipants();if (available.length === 0) {alert('没有合适的抽奖人选!');return;}isRolling = true;startDrawBtn.disabled = true;rollingSound.currentTime = 0;rollingSound.play();const numWinners = parseInt(document.querySelector('input[name="num_winners"]').value);const winners = getRandomWinners(available, numWinners);const finalWinner = winners[0]; let duration = 3000; let interval = 50; let startTime = Date.now();function roll() {let currentTime = Date.now();let elapsed = currentTime - startTime;interval = Math.min(500, 50 + (elapsed / duration) * 450);if (elapsed >= duration - interval) {showParticipant(finalWinner);rollingName.classList.add('winner-highlight');rollingSound.pause();winnerSound.play();showCelebration(finalWinner);const winnerIdsInput = document.createElement('input');winnerIdsInput.type = 'hidden';winnerIdsInput.name = 'winner_ids';winnerIdsInput.value = winners.map(w => w.id).join(',');drawForm.appendChild(winnerIdsInput);setTimeout(() => {rollingName.classList.remove('winner-highlight');drawForm.submit();}, 2000);return;}showParticipant(getRandomParticipant(available));if (elapsed < duration) {setTimeout(roll, interval);}}roll();
});
function showCelebration(winner) {const celebration = document.createElement('div');celebration.className = 'celebration';celebration.innerHTML = `<div class="celebration-content"><div class="celebration-icon">🎉</div><div class="winner-name">${winner.name}</div><div class="winner-dept">${winner.department}</div></div>`;document.querySelector('.lottery-animation').appendChild(celebration);setTimeout(() => {celebration.remove();}, 2000);
}
const ctx = document.getElementById('winnersPieChart').getContext('2d');
new Chart(ctx, {type: 'pie',data: {labels: ['已中奖', '未中奖'],datasets: [{data: [winners.length, participants.length - winners.length],backgroundColor: ['rgba(255, 206, 86, 0.8)','rgba(75, 192, 192, 0.8)']}]},options: {responsive: true,plugins: {legend: {position: 'bottom'},title: {display: true,text: '中奖情况统计'}}}
});
document.querySelectorAll('.name-tag').forEach(tag => {tag.addEventListener('mouseover', () => {tag.style.transform = 'scale(1.1) rotate(5deg)';});tag.addEventListener('mouseout', () => {tag.style.transform = 'translateY(-2px)';});
});
5.index.html
<!DOCTYPE html>
<html data-theme="light">
<head><title>年会抽奖系统</title><link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"><link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"><script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body><div class="theme-switch"><i class="fas fa-moon"></i><label class="switch"><input type="checkbox" id="theme-toggle"><span class="slider round"></span></label><i class="fas fa-sun"></i></div><div class="container"><header><h1><i class="fas fa-gift"></i> 年会抽奖系统</h1><p class="subtitle">让我们看看谁是今天的幸运儿!</p></header>{% with messages = get_flashed_messages() %}{% if messages %}{% for message in messages %}<div class="alert"><i class="fas fa-bell"></i>{{ message }}</div>{% endfor %}{% endif %}{% endwith %}<div class="section"><h2><i class="fas fa-user-plus"></i> 添加参与者</h2><form method="POST" action="{{ url_for('add_participant') }}" class="form-inline"><div class="input-group"><input type="text" name="name" placeholder="姓名" required><input type="text" name="department" placeholder="部门" required><input type="text" name="employee_id" placeholder="工号" required><button type="submit" class="btn"><i class="fas fa-plus"></i> 添加</button></div></form></div><div class="section draw-section"><h2><i class="fas fa-random"></i> 抽奖</h2><div id="lottery-animation" class="lottery-animation"><div class="lottery-box"><div class="lottery-name" id="rolling-name">准备开始</div></div></div><form id="draw-form" method="POST" action="{{ url_for('draw') }}" class="form-inline"><div class="draw-controls"><div class="input-group"><label>抽取人数:</label><input type="number" name="num_winners" value="1" min="1" required></div><div class="checkbox-group"><label><input type="checkbox" name="exclude_winners" value="true" checked><span>排除已中奖者</span></label></div><button type="button" class="btn btn-primary" id="start-draw"><i class="fas fa-dice"></i> 开始抽奖</button></div></form><form method="POST" action="{{ url_for('reset') }}" class="reset-form"><button type="submit" class="btn btn-danger"><i class="fas fa-redo"></i> 重置中奖记录</button></form></div><div class="dashboard"><div class="section participants-section"><h2><i class="fas fa-users"></i>参与者名单<span class="count">({{ participants|length }}人)</span></h2>{% for dept, members in participants|groupby('department') %}<div class="department-group"><h3>{{ dept }} ({{ members|length }}人)</h3><div class="name-list">{% for p in members %}<span class="name-tag {% if p.is_winner %}winner{% endif %}"><i class="fas fa-user"></i>{{ p.name }}<small>{{ p.employee_id }}</small></span>{% endfor %}</div></div>{% endfor %}</div><div class="section winners-section"><h2><i class="fas fa-crown"></i>中奖名单<span class="count">({{ winners|length }}人)</span></h2><div class="name-list">{% for winner in winners %}<span class="name-tag winner"><i class="fas fa-star"></i>{{ winner.name }}</span>{% endfor %}</div></div><div class="section stats-section"><h2><i class="fas fa-chart-pie"></i> 数据统计</h2><div class="charts-container"><div class="chart-wrapper"><canvas id="winnersPieChart"></canvas></div><div class="chart-wrapper"><canvas id="departmentChart"></canvas></div></div></div></div></div><footer><p>祝大家好运!</p></footer><audio id="rolling-sound" preload="auto"></audio><audio id="winner-sound" preload="auto"></audio><script>const participants = {{ participants|tojson|safe }};const winners = {{ winners|tojson|safe }};</script><script src="{{ url_for('static', filename='script.js') }}"></script>
</body>
</html>
5.lucky_draw.py
from flask import Flask, render_template, request, jsonify, redirect, url_for, flash
import mysql.connector
import random
from config import DB_CONFIG
app = Flask(__name__)
app.secret_key = 'your_secret_key'
class Database:def __init__(self):self.conn = Noneself.connect()def connect(self):try:self.conn = mysql.connector.connect(**DB_CONFIG)except Exception as e:print(f"数据库连接错误:{str(e)}")def get_connection(self):try:self.conn.ping(reconnect=True, attempts=3, delay=5)except:self.connect()return self.conn
class LuckyDraw:def __init__(self):self.db = Database()def get_all_participants(self):"""获取所有参与者"""conn = self.db.get_connection()cursor = conn.cursor(dictionary=True)try:cursor.execute("""SELECT p.*, CASE WHEN w.id IS NOT NULL THEN 1 ELSE 0 END as is_winner FROM participants p LEFT JOIN winners w ON p.id = w.participant_idORDER BY p.department, p.name""")return cursor.fetchall()except Exception as e:print(f"获取参与者失败:{str(e)}")return []finally:cursor.close()def get_winners(self):"""获取所有中奖者"""conn = self.db.get_connection()cursor = conn.cursor(dictionary=True)try:cursor.execute("""SELECT p.*, w.draw_time FROM winners w JOIN participants p ON w.participant_id = p.id ORDER BY w.draw_time DESC""")return cursor.fetchall()except Exception as e:print(f"获取中奖者失败:{str(e)}")return []finally:cursor.close()def add_participant(self, name, department, employee_id):"""添加参与者"""conn = self.db.get_connection()cursor = conn.cursor()try:cursor.execute("""INSERT INTO participants (name, department, employee_id) VALUES (%s, %s, %s)""", (name, department, employee_id))conn.commit()return Trueexcept Exception as e:print(f"添加参与者失败:{str(e)}")conn.rollback()return Falsefinally:cursor.close()def draw(self, num_winners=1, exclude_winners=True):"""抽奖"""conn = self.db.get_connection()cursor = conn.cursor(dictionary=True)try:if exclude_winners:cursor.execute("""SELECT p.* FROM participants p LEFT JOIN winners w ON p.id = w.participant_id WHERE w.id IS NULL""")else:cursor.execute("SELECT * FROM participants")available = cursor.fetchall()if not available:return []winners = random.sample(available, min(num_winners, len(available)))for winner in winners:cursor.execute("""INSERT INTO winners (participant_id) VALUES (%s)""", (winner['id'],))conn.commit()return winnersexcept Exception as e:print(f"抽奖失败:{str(e)}")conn.rollback()return []finally:cursor.close()def reset_winners(self):"""重置中奖记录"""conn = self.db.get_connection()cursor = conn.cursor()try:cursor.execute("TRUNCATE TABLE winners")conn.commit()return Trueexcept Exception as e:print(f"重置中奖记录失败:{str(e)}")conn.rollback()return Falsefinally:cursor.close()
lucky_draw = LuckyDraw()
@app.route('/')
def index():"""首页"""participants = lucky_draw.get_all_participants()winners = lucky_draw.get_winners()department_stats = {}for p in participants:dept = p['department']if dept not in department_stats:department_stats[dept] = {'total': 0, 'winners': 0}department_stats[dept]['total'] += 1if p['is_winner']:department_stats[dept]['winners'] += 1return render_template('index.html',participants=participants,winners=winners,department_stats=department_stats)
@app.route('/add_participant', methods=['POST'])
def add_participant():"""添加参与者"""name = request.form.get('name', '').strip()department = request.form.get('department', '').strip()employee_id = request.form.get('employee_id', '').strip()if name and department and employee_id:if lucky_draw.add_participant(name, department, employee_id):flash(f'成功添加参与者:{name}')else:flash('添加参与者失败')else:flash('请填写完整信息')return redirect(url_for('index'))
@app.route('/draw', methods=['POST'])
def draw():"""进行抽奖"""num_winners = int(request.form.get('num_winners', 1))exclude_winners = request.form.get('exclude_winners', 'true') == 'true'winner_ids = request.form.get('winner_ids', '').split(',')if winner_ids and winner_ids[0]:conn = lucky_draw.db.get_connection()cursor = conn.cursor()try:for winner_id in winner_ids:cursor.execute("""INSERT INTO winners (participant_id) VALUES (%s)""", (int(winner_id),))conn.commit()cursor.execute("""SELECT name FROM participants WHERE id IN (%s)""" % ','.join(['%s'] * len(winner_ids)), tuple(map(int, winner_ids)))winner_names = [row[0] for row in cursor.fetchall()]flash(f'恭喜中奖者:{", ".join(winner_names)}')except Exception as e:print(f"记录中奖失败:{str(e)}")conn.rollback()flash('抽奖过程出现错误')finally:cursor.close()else:flash('没有合适的抽奖人选')return redirect(url_for('index'))
@app.route('/reset', methods=['POST'])
def reset():"""重置中奖记录"""if lucky_draw.reset_winners():flash('已重置所有中奖记录')else:flash('重置失败')return redirect(url_for('index'))
if __name__ == '__main__':app.run(debug=True)