
目录结构
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 = None
self.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_id
ORDER 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 True
except Exception as e:
print(f"添加参与者失败:{str(e)}")
conn.rollback()
return False
finally:
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 winners
except 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 True
except Exception as e:
print(f"重置中奖记录失败:{str(e)}")
conn.rollback()
return False
finally:
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'] += 1
if p['is_winner']:
department_stats[dept]['winners'] += 1
return 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)