

一、背景需求
前期制作真实手机号的《破译电话号码(点数对应)》每位幼儿两个手机号码。
现在我想把纸质版变成html版
二、破译电话号码-第1版
【教学类-07-11】20250911小2班破译电话号码(基于亲属不同身份添加头像)
https://mp.csdn.net/mp_blog/creation/editor/151422725


















好处是,纸类破译电话号码必须做两个。但是孩子通APP上孩子的家长手机号从1-6个不等(必须至少留一个手机号)。所以纸类一般用前两个手机号码,如果只有1个号码,就把号码复制一次。但是有些孩子似乎对两个手机号不熟,更熟悉第3、4个手机号。
电子学具:可以把孩子通上幼儿的家长留的所有手机号都做成表格,做成html里面可以破译的号码。
三、破译电话号码-第2版





手机效果很好,但是上面的数字点点的按钮高度不同,导致下面的数字排列也不是一行的。











为了11个数字都横向排列,只能做成非常宽的手机屏(折叠手机?)
四、制作幼儿学号卡片
做36个学号




学号01的第一个号码


学号01的第二个号码















python
'''
36个学号卡、宽屏手机(11个数字横排)
重新打开还是白色学号
deepseek,阿夏
20261027
'''
import os
import pandas as pd
def generate_phone_crack_game(excel_path, save_html_path):
"""
读取 Excel 数据,生成《破译电话号码》的 HTML 游戏文件。
模式:先展示 9×4 学号卡片矩阵 → 点击卡片进入手机界面 → 完成两个号码后自动返回,卡片置灰。
(每次刷新/重新打开页面,状态自动重置为全白)
"""
print(f'正在读取 Excel 文件: {excel_path}')
# 1. 读取 Excel 数据
if not os.path.exists(excel_path):
print(f'❌ 找不到 Excel 文件: {excel_path}')
return
try:
df = pd.read_excel(excel_path, dtype=str)
df = df.fillna('')
student_list = []
for _, row in df.iterrows():
s_id = str(row.get('学号', '')).strip()
s_name = str(row.get('姓名', '')).strip()
s_id1 = str(row.get('身份1', '')).strip()
s_phone1 = str(row.get('手机号码1', '')).strip()
s_id2 = str(row.get('身份2', '')).strip()
s_phone2 = str(row.get('手机号码2', '')).strip()
if not s_id and not s_name:
continue
if s_phone1.endswith('.0'):
s_phone1 = s_phone1[:-2]
if s_phone2.endswith('.0'):
s_phone2 = s_phone2[:-2]
item_str = (
f'{{ id: "{s_id}", name: "{s_name}", id1: "{s_id1}", '
f'phone1: "{s_phone1}", id2: "{s_id2}", phone2: "{s_phone2}" }}'
)
student_list.append(item_str)
js_student_data = "[\n " + ",\n ".join(student_list) + "\n ]"
print(f'成功解析 {len(student_list)} 名幼儿的数据。')
except Exception as e:
print(f'❌ 读取 Excel 失败: {e}')
return
# 2. 构造 HTML 内容
html_content = f'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>破译电话号码</title>
<style>
* {{
box-sizing: border-box;
user-select: none;
-webkit-tap-highlight-color: transparent;
margin: 0;
padding: 0;
}}
body {{
font-family: 'Comic Sans MS', 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
touch-action: none;
padding: 10px;
}}
/* ====== 视图容器 ====== */
.view {{
display: none;
width: 100%;
height: 100%;
max-width: 800px;
max-height: 850px;
flex-direction: column;
align-items: center;
justify-content: center;
}}
.view.active {{
display: flex;
}}
/* ====== 卡片矩阵视图 ====== */
.card-grid-wrapper {{
width: 100%;
height: 100%;
max-height: 850px;
background: #f1f3f5;
border-radius: 40px;
box-shadow: 0 30px 40px rgba(0,0,0,0.2), inset 0 0 0 8px #2d2d2d, inset 0 0 0 12px #5c5c5c;
padding: 24px 20px;
display: flex;
flex-direction: column;
overflow: hidden;
border: 2px solid #aaa;
}}
.grid-title {{
font-size: 1.6rem;
font-weight: bold;
color: #073b4c;
text-align: center;
margin-bottom: 16px;
background: #ffd166;
padding: 10px 0;
border-radius: 50px;
box-shadow: 0 4px 0 #c79a3e;
flex-shrink: 0;
}}
.card-grid {{
flex: 1;
display: grid;
grid-template-columns: repeat(9, 1fr);
grid-template-rows: repeat(4, 1fr);
gap: 10px;
overflow-y: auto;
padding: 6px;
background: #e9ecef;
border-radius: 20px;
align-content: start;
}}
.student-card {{
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: #ffffff;
border-radius: 14px;
font-size: 1rem;
font-weight: bold;
color: #118ab2;
box-shadow: 0 4px 0 #cbd5e1, 0 2px 6px rgba(0,0,0,0.08);
cursor: pointer;
transition: all 0.15s;
border: 2px solid transparent;
padding: 4px;
text-align: center;
overflow: hidden;
}}
.student-card .card-name {{
font-size: 1rem;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}}
.student-card .card-id {{
font-size: 1.6rem; /* 学号字体适当调大,显示更清晰 */
color: #666;
font-weight: 900;
margin-top: 2px;
}}
.student-card:active {{
transform: translateY(4px);
box-shadow: 0 0 0 #cbd5e1, 0 2px 6px rgba(0,0,0,0.08);
}}
/* 已完成的卡片置灰 */
.student-card.done {{
background: #ced4da;
color: #888;
box-shadow: 0 2px 0 #adb5bd;
pointer-events: none;
opacity: 0.7;
border-color: #adb5bd;
}}
.student-card.done .card-name {{
color: #666;
}}
.student-card.done .card-id {{
color: #999;
}}
/* ====== 手机视图 ====== */
.phone-shell {{
width: 100%;
height: 100%;
max-height: 850px;
background: #f1f3f5;
border-radius: 48px;
box-shadow: 0 30px 40px rgba(0,0,0,0.2), inset 0 0 0 8px #2d2d2d, inset 0 0 0 12px #5c5c5c;
padding: 20px 22px 18px;
display: flex;
flex-direction: column;
transition: all 0.2s;
overflow: hidden;
border: 2px solid #aaa;
position: relative;
}}
.status-bar {{
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 8px 8px;
font-size: 0.9rem;
color: #3a3a3a;
font-weight: bold;
background: transparent;
flex-shrink: 0;
}}
.status-bar .time {{
font-size: 1.2rem;
}}
.status-bar .student-badge {{
background: #ffb703;
padding: 6px 16px;
border-radius: 40px;
font-size: 1rem;
color: #fff;
box-shadow: 0 2px 0 #c77d00;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}}
.screen {{
flex: 1;
background: #ffffff;
border-radius: 28px;
padding: 16px 18px 10px;
box-shadow: inset 0 0 0 2px #e0e0e0, 0 6px 0 #b0b0b0;
display: flex;
flex-direction: column;
overflow: hidden;
margin-bottom: 12px;
min-height: 0;
}}
.screen-header {{
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 4px 12px;
font-size: 1.1rem;
font-weight: bold;
color: #118ab2;
flex-shrink: 0;
}}
.screen-header .phone-label {{
background: #e0f2fe;
padding: 8px 18px;
border-radius: 30px;
}}
.screen-header .identity-tag {{
background: #ffd166;
padding: 8px 20px;
border-radius: 30px;
color: #333;
display: flex;
align-items: center;
gap: 6px;
}}
.dots-scroll {{
flex: 1;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
justify-content: space-between;
gap: 6px;
padding: 10px 6px;
background: #f8fafc;
border-radius: 20px;
border: 2px dashed #cfd8dc;
overflow: hidden;
min-height: 0;
}}
.dot-group {{
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
max-width: 60px;
min-width: 0;
transition: all 0.2s ease;
}}
.dot-box {{
display: flex;
flex-wrap: wrap;
justify-content: center;
align-content: center;
gap: 3px;
background: #fff;
padding: 6px;
border-radius: 12px;
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
width: 100%;
aspect-ratio: 1 / 1;
max-width: 52px;
border: 2px solid transparent;
transition: 0.2s;
}}
.dot-group.active .dot-box {{
border: 3px solid #06d6a0;
transform: scale(1.1);
box-shadow: 0 0 12px rgba(6, 214, 160, 0.5);
z-index: 2;
}}
.dot {{
width: 8px;
height: 8px;
background-color: #ef476f;
border-radius: 50%;
display: inline-block;
}}
.dot.zero {{
background-color: transparent;
border: 3px solid #ef476f;
width: 12px;
height: 12px;
}}
.dot-number {{
margin-top: 6px;
font-size: 1.4rem;
font-weight: 800;
color: #073b4c;
height: 1.6rem;
line-height: 1.6rem;
text-align: center;
opacity: 0;
transform: scale(0.5);
transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}}
.dot-number.show {{
opacity: 1;
transform: scale(1);
}}
.hint-text {{
font-size: 1.1rem;
color: #ff6b6b;
text-align: center;
margin-top: 10px;
height: 26px;
font-weight: bold;
flex-shrink: 0;
}}
.keypad {{
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px 16px;
padding: 6px 8px 0;
flex-shrink: 0;
}}
.key {{
background: #ffffff;
border: none;
border-radius: 20px;
font-size: 2.4rem;
font-weight: 700;
color: #118ab2;
padding: 14px 0;
cursor: pointer;
box-shadow: 0 6px 0 #cbd5e1, 0 4px 10px rgba(0,0,0,0.05);
transition: all 0.08s;
text-align: center;
line-height: 1;
font-family: inherit;
border: 1px solid #e2e8f0;
}}
.key:active {{
transform: translateY(6px);
box-shadow: 0 0 0 #cbd5e1, 0 4px 10px rgba(0,0,0,0.05);
}}
.key.special {{
background: #e2e8f0;
color: #4a5568;
font-size: 2rem;
box-shadow: 0 6px 0 #a0aec0, 0 4px 10px rgba(0,0,0,0.05);
}}
.key.special:active {{
box-shadow: 0 0 0 #a0aec0, 0 4px 10px rgba(0,0,0,0.05);
}}
.next-btn {{
background: #06d6a0;
border: none;
border-radius: 40px;
color: #fff;
font-weight: bold;
font-size: 1.1rem;
padding: 10px 20px;
box-shadow: 0 4px 0 #048a67;
cursor: pointer;
transition: 0.08s;
font-family: inherit;
white-space: nowrap;
margin-left: 10px;
}}
.next-btn:active {{
transform: translateY(4px);
box-shadow: 0 0 0 #048a67;
}}
/* 弹窗 */
.modal {{
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.6);
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s;
z-index: 100;
padding: 20px;
}}
.modal.show {{
opacity: 1;
pointer-events: auto;
}}
.modal-content {{
background: #fff;
border-radius: 40px;
padding: 40px 30px;
text-align: center;
max-width: 420px;
width: 100%;
transform: scale(0.85);
transition: transform 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
box-shadow: 0 20px 30px rgba(0,0,0,0.2);
}}
.modal.show .modal-content {{
transform: scale(1);
}}
.avatar {{
font-size: 6rem;
margin-bottom: 16px;
display: inline-block;
animation: bounce 2s infinite;
}}
@keyframes bounce {{
0%, 100% {{ transform: translateY(0); }}
50% {{ transform: translateY(-12px); }}
}}
.speech-bubble {{
background: #e0f7fa;
border-radius: 24px;
padding: 20px;
font-size: 1.3rem;
color: #006064;
position: relative;
margin-bottom: 26px;
border: 3px solid #4dd0e1;
line-height: 1.5;
word-break: break-word;
}}
.speech-bubble::before {{
content: '';
position: absolute;
top: -18px;
left: 50%;
transform: translateX(-50%);
border-width: 0 16px 16px 16px;
border-style: solid;
border-color: transparent transparent #4dd0e1 transparent;
}}
.btn-next {{
background: #06d6a0;
color: white;
border: none;
padding: 16px 40px;
font-size: 1.3rem;
border-radius: 50px;
cursor: pointer;
box-shadow: 0 6px 0 #048a67;
font-weight: bold;
font-family: inherit;
transition: 0.08s;
}}
.btn-next:active {{
transform: translateY(6px);
box-shadow: 0 0 0 #048a67;
}}
@keyframes shake {{
0%, 100% {{ transform: translateX(0); }}
25% {{ transform: translateX(-8px); }}
50% {{ transform: translateX(8px); }}
75% {{ transform: translateX(-8px); }}
}}
.shake {{
animation: shake 0.35s;
}}
/* 小屏适配 */
@media (max-width: 768px) {{
.phone-shell {{ padding: 15px 15px 12px; }}
.dot {{ width: 6px; height: 6px; }}
.dot.zero {{ width: 9px; height: 9px; border-width: 2px; }}
.dot-box {{ gap: 2px; padding: 4px; }}
.key {{ font-size: 1.8rem; padding: 10px 0; }}
.key.special {{ font-size: 1.5rem; }}
.dot-number {{ font-size: 1.1rem; height: 1.4rem; line-height: 1.4rem; }}
.dots-scroll {{ gap: 4px; padding: 6px 4px; }}
.screen {{ padding: 12px 10px 8px; }}
.card-grid {{ gap: 6px; }}
.student-card .card-name {{ font-size: 0.75rem; }}
.student-card .card-id {{ font-size: 1.1rem; }}
}}
</style>
</head>
<body>
<!-- ================= 视图 1:学号卡片矩阵 ================= -->
<div class="view active" id="viewGrid">
<div class="card-grid-wrapper">
<div class="grid-title">📋 大3班《破译电话号码》选一个学号破译吧</div>
<div class="card-grid" id="cardGrid">
<!-- 动态渲染卡片 -->
</div>
</div>
</div>
<!-- ================= 视图 2:手机破译界面 ================= -->
<div class="view" id="viewPhone">
<div class="phone-shell">
<div class="status-bar">
<div class="time">📞 破译</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span class="student-badge" id="studentBadge">小朋友</span>
<button class="next-btn" onclick="backToGrid()">返回</button>
</div>
</div>
<div class="screen">
<div class="screen-header">
<div class="phone-label" id="phoneLabel">第1个号码</div>
<div class="identity-tag">
<span id="identityEmoji">👩</span>
<span id="identityName">妈妈</span>
</div>
</div>
<div class="dots-scroll" id="dotsContainer">
<!-- 动态渲染点阵 -->
</div>
<div class="hint-text" id="hintText">请观察有几个点,在下面按数字哦!</div>
</div>
<div class="keypad" id="keypad">
<!-- 动态渲染键盘 -->
</div>
</div>
</div>
<!-- 拨号弹窗 -->
<div class="modal" id="callModal">
<div class="modal-content">
<div class="avatar" id="modalAvatar">📞</div>
<div class="speech-bubble" id="modalSpeech">正在拨号...</div>
<button class="btn-next" id="modalBtn" onclick="closeModal()" style="display: none;">听到啦,继续</button>
</div>
</div>
<script>
// ------------------------------------------------------------------
// 学生数据(由 Python 注入)
// ------------------------------------------------------------------
const studentData = {js_student_data};
// 家人头像库
const avatars = {{
"妈妈": "👩", "爸爸": "👨", "奶奶": "👵", "爷爷": "👴",
"外婆": "👵", "外公": "👴", "叔叔": "🧑", "阿姨": "👩",
"舅舅": "🧑", "姑姑": "👩"
}};
// 语音台词
const speechLines = [
"{{name}}宝贝,我是{{identity}},宝贝在幼儿园开心吗?",
"{{name}},我是{{identity}}呀,今天有没有乖乖吃饭呀?",
"喂,{{name}}宝贝,我是{{identity}},今天和小朋友玩得开心吗?",
"{{name}},我是{{identity}},今天学了什么新本领呀?",
"宝贝{{name}},我是{{identity}},下班我就来接你啦!"
];
// DOM 元素
const viewGrid = document.getElementById('viewGrid');
const viewPhone = document.getElementById('viewPhone');
const cardGrid = document.getElementById('cardGrid');
const studentBadge = document.getElementById('studentBadge');
const phoneLabel = document.getElementById('phoneLabel');
const identityEmoji = document.getElementById('identityEmoji');
const identityName = document.getElementById('identityName');
const dotsContainer = document.getElementById('dotsContainer');
const hintText = document.getElementById('hintText');
const keypad = document.getElementById('keypad');
const callModal = document.getElementById('callModal');
const modalAvatar = document.getElementById('modalAvatar');
const modalSpeech = document.getElementById('modalSpeech');
const modalBtn = document.getElementById('modalBtn');
// 状态变量
let currentStudent = null;
let currentPhoneIndex = 1;
let currentPhoneStr = "";
let currentIdentity = "";
let currentDotIndex = 0;
let isWaiting = false;
let audioCtx = null;
// 🌟 新增:用于记录本次打开网页后,已完成的学号(不再使用 localStorage)
let completedIds = new Set();
// ---------- 初始化 ----------
function initGame() {{
// 生成键盘
renderKeypad();
// 生成卡片矩阵
renderCardGrid();
try {{
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}} catch (e) {{ /* ignore */ }}
}}
// ---------- 生成 9x4 学号卡片矩阵 ----------
function renderCardGrid() {{
cardGrid.innerHTML = '';
studentData.forEach((student, index) => {{
const card = document.createElement('div');
card.className = 'student-card';
card.dataset.id = student.id;
card.id = `card-${{student.id}}`;
card.innerHTML = `
<div class="card-name">${{student.name}}</div>
<div class="card-id">${{student.id}}</div>
`;
// 检查是否在本次打开记录中已完成
if (completedIds.has(student.id)) {{
card.classList.add('done');
}}
card.addEventListener('click', () => {{
if (card.classList.contains('done')) return;
enterStudent(student);
}});
cardGrid.appendChild(card);
}});
}}
// ---------- 进入某个学生的破译界面 ----------
function enterStudent(student) {{
currentStudent = student;
// 切换视图
viewGrid.classList.remove('active');
viewPhone.classList.add('active');
// 更新顶部信息
studentBadge.innerText = `${{student.name}} · ${{student.id}}`;
// 开始第一个电话
startPhone(1);
}}
// ---------- 返回卡片矩阵 ----------
function backToGrid() {{
viewPhone.classList.remove('active');
viewGrid.classList.add('active');
// 取消语音
if ('speechSynthesis' in window) window.speechSynthesis.cancel();
// 重新渲染卡片(刷新置灰状态)
renderCardGrid();
}}
// ---------- 生成 3x4 键盘 ----------
function renderKeypad() {{
keypad.innerHTML = '';
const keys = [
{{ label: '1', value: 1, type: 'number' }}, {{ label: '2', value: 2, type: 'number' }}, {{ label: '3', value: 3, type: 'number' }},
{{ label: '4', value: 4, type: 'number' }}, {{ label: '5', value: 5, type: 'number' }}, {{ label: '6', value: 6, type: 'number' }},
{{ label: '7', value: 7, type: 'number' }}, {{ label: '8', value: 8, type: 'number' }}, {{ label: '9', value: 9, type: 'number' }},
{{ label: '*', value: '*', type: 'special' }}, {{ label: '0', value: 0, type: 'number' }}, {{ label: '#', value: '#', type: 'special' }}
];
keys.forEach(item => {{
const btn = document.createElement('button');
btn.className = 'key' + (item.type === 'special' ? ' special' : '');
btn.innerText = item.label;
btn.addEventListener('click', () => handleKeyPress(item.value));
keypad.appendChild(btn);
}});
}}
// ---------- 开始某个电话 ----------
function startPhone(index) {{
currentPhoneIndex = index;
currentDotIndex = 0;
isWaiting = false;
if (index === 1) {{
currentPhoneStr = currentStudent.phone1 || '';
currentIdentity = currentStudent.id1 || '家人';
}} else {{
currentPhoneStr = currentStudent.phone2 || '';
currentIdentity = currentStudent.id2 || '家人';
}}
if (!currentPhoneStr) {{
if (index === 1 && currentStudent.phone2) {{
startPhone(2);
}} else {{
completeStudent();
}}
return;
}}
phoneLabel.innerText = index === 1 ? '第1个号码' : '第2个号码';
identityName.innerText = currentIdentity;
identityEmoji.innerText = avatars[currentIdentity] || '👤';
hintText.innerText = '请观察有几个点,在下面按数字哦!';
hintText.style.color = '#ff6b6b';
renderDots();
updateActiveDot();
}}
// ---------- 渲染点阵 ----------
function renderDots() {{
dotsContainer.innerHTML = '';
const digits = currentPhoneStr.split('');
digits.forEach((digit, index) => {{
const group = document.createElement('div');
group.className = 'dot-group';
group.id = `dot-group-${{index}}`;
const box = document.createElement('div');
box.className = 'dot-box';
let num = parseInt(digit, 10);
if (isNaN(num)) num = 0;
if (num === 0) {{
const dot = document.createElement('div');
dot.className = 'dot zero';
box.appendChild(dot);
}} else {{
for (let i = 0; i < num; i++) {{
const dot = document.createElement('div');
dot.className = 'dot';
box.appendChild(dot);
}}
}}
const numText = document.createElement('div');
numText.className = 'dot-number';
numText.innerText = digit;
group.appendChild(box);
group.appendChild(numText);
dotsContainer.appendChild(group);
}});
}}
// ---------- 高亮当前点组 ----------
function updateActiveDot() {{
document.querySelectorAll('.dot-group').forEach(el => el.classList.remove('active'));
if (currentDotIndex < currentPhoneStr.length) {{
const currentGroup = document.getElementById(`dot-group-${{currentDotIndex}}`);
if (currentGroup) {{
currentGroup.classList.add('active');
}}
}}
}}
// ---------- 处理按键 ----------
function handleKeyPress(value) {{
if (isWaiting) return;
if (value === '*' || value === '#') return;
const targetDigit = parseInt(currentPhoneStr[currentDotIndex], 10);
if (value === targetDigit) {{
playSound('correct');
const group = document.getElementById(`dot-group-${{currentDotIndex}}`);
if (group) {{
group.querySelector('.dot-number').classList.add('show');
}}
currentDotIndex++;
if (currentDotIndex < currentPhoneStr.length) {{
updateActiveDot();
}} else {{
isWaiting = true;
setTimeout(() => dialNumber(), 800);
}}
}} else {{
playSound('wrong');
hintText.innerText = '哎呀,不对哦!再数一数,试一次吧!';
hintText.style.color = '#ef476f';
const activeBox = document.querySelector('.dot-group.active .dot-box');
if (activeBox) {{
activeBox.classList.add('shake');
setTimeout(() => activeBox.classList.remove('shake'), 400);
}}
}}
}}
// ---------- 拨号动画 ----------
function dialNumber() {{
callModal.classList.add('show');
modalAvatar.innerText = '📞';
modalSpeech.innerText = '正在拨号...';
modalBtn.style.display = 'none';
playSound('dial');
setTimeout(() => {{
playSound('connect');
modalAvatar.innerText = avatars[currentIdentity] || '👤';
const line = speechLines[Math.floor(Math.random() * speechLines.length)]
.replace(/{{name}}/g, currentStudent.name)
.replace(/{{identity}}/g, currentIdentity);
modalSpeech.innerText = line;
speakText(line);
modalBtn.innerText = currentPhoneIndex === 1 ? '听完了,打第二个电话' : '好啦,完成';
modalBtn.style.display = 'inline-block';
}}, 2000);
}}
// ---------- 关闭弹窗 ----------
function closeModal() {{
callModal.classList.remove('show');
if ('speechSynthesis' in window) {{
window.speechSynthesis.cancel();
}}
if (currentPhoneIndex === 1) {{
startPhone(2);
}} else {{
completeStudent();
}}
}}
// ---------- 完成某个学生,返回卡片矩阵并置灰 ----------
function completeStudent() {{
if (currentStudent) {{
// 🌟 记录到内存中的 Set 集合里,不再使用 localStorage
completedIds.add(currentStudent.id);
}}
// 返回卡片矩阵
backToGrid();
}}
// ---------- 音效 ----------
function playSound(type) {{
if (!audioCtx) return;
if (audioCtx.state === 'suspended') audioCtx.resume();
const now = audioCtx.currentTime;
if (type === 'correct') {{
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.type = 'sine';
osc.frequency.setValueAtTime(880, now);
osc.frequency.exponentialRampToValueAtTime(1318.51, now + 0.1);
gain.gain.setValueAtTime(0.3, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.2);
osc.start(now);
osc.stop(now + 0.2);
}} else if (type === 'wrong') {{
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(150, now);
osc.frequency.linearRampToValueAtTime(100, now + 0.2);
gain.gain.setValueAtTime(0.3, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
osc.start(now);
osc.stop(now + 0.3);
}} else if (type === 'dial') {{
const osc1 = audioCtx.createOscillator();
const gain1 = audioCtx.createGain();
osc1.connect(gain1);
gain1.connect(audioCtx.destination);
osc1.type = 'sine';
osc1.frequency.setValueAtTime(440, now);
gain1.gain.setValueAtTime(0.1, now);
osc1.start(now);
osc1.stop(now + 0.5);
setTimeout(() => {{
const osc2 = audioCtx.createOscillator();
const gain2 = audioCtx.createGain();
osc2.connect(gain2);
gain2.connect(audioCtx.destination);
osc2.type = 'sine';
osc2.frequency.setValueAtTime(440, audioCtx.currentTime);
gain2.gain.setValueAtTime(0.1, audioCtx.currentTime);
osc2.start(audioCtx.currentTime);
osc2.stop(audioCtx.currentTime + 0.5);
}}, 1000);
}} else if (type === 'connect') {{
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.type = 'triangle';
osc.frequency.setValueAtTime(523.25, now);
gain.gain.setValueAtTime(0.2, now);
osc.start(now);
osc.stop(now + 0.1);
setTimeout(() => {{
const osc2 = audioCtx.createOscillator();
const gain2 = audioCtx.createGain();
osc2.connect(gain2);
gain2.connect(audioCtx.destination);
osc2.type = 'triangle';
osc2.frequency.setValueAtTime(659.25, audioCtx.currentTime);
gain2.gain.setValueAtTime(0.2, audioCtx.currentTime);
osc2.start(audioCtx.currentTime);
osc2.stop(audioCtx.currentTime + 0.15);
}}, 120);
}}
}}
// ---------- 语音合成 ----------
function speakText(text) {{
if ('speechSynthesis' in window) {{
window.speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = 'zh-CN';
utterance.rate = 0.9;
utterance.pitch = 1.2;
const voices = window.speechSynthesis.getVoices();
const zhVoice = voices.find(v => v.lang.includes('zh'));
if (zhVoice) utterance.voice = zhVoice;
window.speechSynthesis.speak(utterance);
}}
}}
// 启动
window.onload = initGame;
</script>
</body>
</html>'''
# 3. 保存并输出
try:
os.makedirs(os.path.dirname(save_html_path), exist_ok=True)
with open(save_html_path, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f'✅ 破译电话号码游戏已成功生成!')
print(f'📁 文件保存位置: {save_html_path}')
except Exception as e:
print(f'❌ 生成 HTML 文件失败: {e}')
if __name__ == '__main__':
# ================= 配置区 =================
path = r'D:\000Python网页html\20261026破译电话号码'
EXCEL_PATH = path + r'\家长手机号码.xlsx'
SAVE_PATH = path + r'\006破译电话号码_自动重置版.html'
# =========================================
generate_phone_crack_game(EXCEL_PATH, SAVE_PATH)






python
'''
36个学号卡、宽屏手机(11个数字横排)
重新打开还是白色学号
deepseek,阿夏
20261027
'''
import os
import pandas as pd
def generate_phone_crack_game(excel_path, save_html_path):
"""
读取 Excel 数据,生成《破译电话号码》的 HTML 游戏文件。
模式:先展示 9×4 学号卡片矩阵 → 点击卡片进入手机界面 → 完成两个号码后自动返回,卡片置灰。
(每次刷新/重新打开页面,状态自动重置为全白)
"""
print(f'正在读取 Excel 文件: {excel_path}')
# 1. 读取 Excel 数据
if not os.path.exists(excel_path):
print(f'❌ 找不到 Excel 文件: {excel_path}')
return
try:
df = pd.read_excel(excel_path, dtype=str)
df = df.fillna('')
student_list = []
for _, row in df.iterrows():
s_id = str(row.get('学号', '')).strip()
s_name = str(row.get('姓名', '')).strip()
s_id1 = str(row.get('身份1', '')).strip()
s_phone1 = str(row.get('手机号码1', '')).strip()
s_id2 = str(row.get('身份2', '')).strip()
s_phone2 = str(row.get('手机号码2', '')).strip()
if not s_id and not s_name:
continue
if s_phone1.endswith('.0'):
s_phone1 = s_phone1[:-2]
if s_phone2.endswith('.0'):
s_phone2 = s_phone2[:-2]
item_str = (
f'{{ id: "{s_id}", name: "{s_name}", id1: "{s_id1}", '
f'phone1: "{s_phone1}", id2: "{s_id2}", phone2: "{s_phone2}" }}'
)
student_list.append(item_str)
js_student_data = "[\n " + ",\n ".join(student_list) + "\n ]"
print(f'成功解析 {len(student_list)} 名幼儿的数据。')
except Exception as e:
print(f'❌ 读取 Excel 失败: {e}')
return
# 2. 构造 HTML 内容
html_content = f'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>破译电话号码</title>
<style>
* {{
box-sizing: border-box;
user-select: none;
-webkit-tap-highlight-color: transparent;
margin: 0;
padding: 0;
}}
body {{
font-family: 'Comic Sans MS', 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
touch-action: none;
padding: 10px;
}}
/* ====== 视图容器 ====== */
.view {{
display: none;
width: 100%;
height: 100%;
max-width: 800px;
max-height: 850px;
flex-direction: column;
align-items: center;
justify-content: center;
}}
.view.active {{
display: flex;
}}
/* ====== 卡片矩阵视图 ====== */
.card-grid-wrapper {{
width: 100%;
height: 100%;
max-height: 850px;
background: #f1f3f5;
border-radius: 40px;
box-shadow: 0 30px 40px rgba(0,0,0,0.2), inset 0 0 0 8px #2d2d2d, inset 0 0 0 12px #5c5c5c;
padding: 24px 20px;
display: flex;
flex-direction: column;
overflow: hidden;
border: 2px solid #aaa;
}}
.grid-title {{
font-size: 1.6rem;
font-weight: bold;
color: #073b4c;
text-align: center;
margin-bottom: 16px;
background: #ffd166;
padding: 10px 0;
border-radius: 50px;
box-shadow: 0 4px 0 #c79a3e;
flex-shrink: 0;
}}
.card-grid {{
flex: 1;
display: grid;
grid-template-columns: repeat(9, 1fr);
grid-template-rows: repeat(4, 1fr);
gap: 10px;
overflow-y: auto;
padding: 6px;
background: #e9ecef;
border-radius: 20px;
align-content: start;
}}
.student-card {{
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: #ffffff;
border-radius: 14px;
font-size: 1rem;
font-weight: bold;
color: #118ab2;
box-shadow: 0 4px 0 #cbd5e1, 0 2px 6px rgba(0,0,0,0.08);
cursor: pointer;
transition: all 0.15s;
border: 2px solid transparent;
padding: 4px;
text-align: center;
overflow: hidden;
}}
.student-card .card-name {{
font-size: 1rem;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}}
.student-card .card-id {{
font-size: 1.6rem; /* 学号字体适当调大,显示更清晰 */
color: #666;
font-weight: 900;
margin-top: 2px;
}}
.student-card:active {{
transform: translateY(4px);
box-shadow: 0 0 0 #cbd5e1, 0 2px 6px rgba(0,0,0,0.08);
}}
/* 已完成的卡片置灰 */
.student-card.done {{
background: #ced4da;
color: #888;
box-shadow: 0 2px 0 #adb5bd;
pointer-events: none;
opacity: 0.7;
border-color: #adb5bd;
}}
.student-card.done .card-name {{
color: #666;
}}
.student-card.done .card-id {{
color: #999;
}}
/* ====== 手机视图 ====== */
.phone-shell {{
width: 100%;
height: 100%;
max-height: 850px;
background: #f1f3f5;
border-radius: 48px;
box-shadow: 0 30px 40px rgba(0,0,0,0.2), inset 0 0 0 8px #2d2d2d, inset 0 0 0 12px #5c5c5c;
padding: 20px 22px 18px;
display: flex;
flex-direction: column;
transition: all 0.2s;
overflow: hidden;
border: 2px solid #aaa;
position: relative;
}}
.status-bar {{
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 8px 8px;
font-size: 0.9rem;
color: #3a3a3a;
font-weight: bold;
background: transparent;
flex-shrink: 0;
}}
.status-bar .time {{
font-size: 1.2rem;
}}
.status-bar .student-badge {{
background: #ffb703;
padding: 6px 16px;
border-radius: 40px;
font-size: 1rem;
color: #fff;
box-shadow: 0 2px 0 #c77d00;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}}
.screen {{
flex: 1;
background: #ffffff;
border-radius: 28px;
padding: 16px 18px 10px;
box-shadow: inset 0 0 0 2px #e0e0e0, 0 6px 0 #b0b0b0;
display: flex;
flex-direction: column;
overflow: hidden;
margin-bottom: 12px;
min-height: 0;
}}
.screen-header {{
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 4px 12px;
font-size: 1.1rem;
font-weight: bold;
color: #118ab2;
flex-shrink: 0;
}}
.screen-header .phone-label {{
background: #e0f2fe;
padding: 8px 18px;
border-radius: 30px;
}}
.screen-header .identity-tag {{
background: #ffd166;
padding: 8px 20px;
border-radius: 30px;
color: #333;
display: flex;
align-items: center;
gap: 6px;
}}
.dots-scroll {{
flex: 1;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
justify-content: space-between;
gap: 6px;
padding: 10px 6px;
background: #f8fafc;
border-radius: 20px;
border: 2px dashed #cfd8dc;
overflow: hidden;
min-height: 0;
}}
.dot-group {{
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
max-width: 60px;
min-width: 0;
transition: all 0.2s ease;
}}
.dot-box {{
display: flex;
flex-wrap: wrap;
justify-content: center;
align-content: center;
gap: 3px;
background: #fff;
padding: 6px;
border-radius: 12px;
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
width: 100%;
aspect-ratio: 1 / 1;
max-width: 52px;
border: 2px solid transparent;
transition: 0.2s;
}}
.dot-group.active .dot-box {{
border: 3px solid #06d6a0;
transform: scale(1.1);
box-shadow: 0 0 12px rgba(6, 214, 160, 0.5);
z-index: 2;
}}
.dot {{
width: 8px;
height: 8px;
background-color: #ef476f;
border-radius: 50%;
display: inline-block;
}}
.dot.zero {{
background-color: transparent;
border: 3px solid #ef476f;
width: 12px;
height: 12px;
}}
.dot-number {{
margin-top: 6px;
font-size: 1.4rem;
font-weight: 800;
color: #073b4c;
height: 1.6rem;
line-height: 1.6rem;
text-align: center;
opacity: 0;
transform: scale(0.5);
transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}}
.dot-number.show {{
opacity: 1;
transform: scale(1);
}}
.hint-text {{
font-size: 1.1rem;
color: #ff6b6b;
text-align: center;
margin-top: 10px;
height: 26px;
font-weight: bold;
flex-shrink: 0;
}}
.keypad {{
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px 16px;
padding: 6px 8px 0;
flex-shrink: 0;
}}
.key {{
background: #ffffff;
border: none;
border-radius: 20px;
font-size: 2.4rem;
font-weight: 700;
color: #118ab2;
padding: 14px 0;
cursor: pointer;
box-shadow: 0 6px 0 #cbd5e1, 0 4px 10px rgba(0,0,0,0.05);
transition: all 0.08s;
text-align: center;
line-height: 1;
font-family: inherit;
border: 1px solid #e2e8f0;
}}
.key:active {{
transform: translateY(6px);
box-shadow: 0 0 0 #cbd5e1, 0 4px 10px rgba(0,0,0,0.05);
}}
.key.special {{
background: #e2e8f0;
color: #4a5568;
font-size: 2rem;
box-shadow: 0 6px 0 #a0aec0, 0 4px 10px rgba(0,0,0,0.05);
}}
.key.special:active {{
box-shadow: 0 0 0 #a0aec0, 0 4px 10px rgba(0,0,0,0.05);
}}
.next-btn {{
background: #06d6a0;
border: none;
border-radius: 40px;
color: #fff;
font-weight: bold;
font-size: 1.1rem;
padding: 10px 20px;
box-shadow: 0 4px 0 #048a67;
cursor: pointer;
transition: 0.08s;
font-family: inherit;
white-space: nowrap;
margin-left: 10px;
}}
.next-btn:active {{
transform: translateY(4px);
box-shadow: 0 0 0 #048a67;
}}
/* 弹窗 */
.modal {{
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.6);
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s;
z-index: 100;
padding: 20px;
}}
.modal.show {{
opacity: 1;
pointer-events: auto;
}}
.modal-content {{
background: #fff;
border-radius: 40px;
padding: 40px 30px;
text-align: center;
max-width: 420px;
width: 100%;
transform: scale(0.85);
transition: transform 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
box-shadow: 0 20px 30px rgba(0,0,0,0.2);
}}
.modal.show .modal-content {{
transform: scale(1);
}}
.avatar {{
font-size: 6rem;
margin-bottom: 16px;
display: inline-block;
animation: bounce 2s infinite;
}}
@keyframes bounce {{
0%, 100% {{ transform: translateY(0); }}
50% {{ transform: translateY(-12px); }}
}}
.speech-bubble {{
background: #e0f7fa;
border-radius: 24px;
padding: 20px;
font-size: 1.3rem;
color: #006064;
position: relative;
margin-bottom: 26px;
border: 3px solid #4dd0e1;
line-height: 1.5;
word-break: break-word;
}}
.speech-bubble::before {{
content: '';
position: absolute;
top: -18px;
left: 50%;
transform: translateX(-50%);
border-width: 0 16px 16px 16px;
border-style: solid;
border-color: transparent transparent #4dd0e1 transparent;
}}
.btn-next {{
background: #06d6a0;
color: white;
border: none;
padding: 16px 40px;
font-size: 1.3rem;
border-radius: 50px;
cursor: pointer;
box-shadow: 0 6px 0 #048a67;
font-weight: bold;
font-family: inherit;
transition: 0.08s;
}}
.btn-next:active {{
transform: translateY(6px);
box-shadow: 0 0 0 #048a67;
}}
@keyframes shake {{
0%, 100% {{ transform: translateX(0); }}
25% {{ transform: translateX(-8px); }}
50% {{ transform: translateX(8px); }}
75% {{ transform: translateX(-8px); }}
}}
.shake {{
animation: shake 0.35s;
}}
/* 小屏适配 */
@media (max-width: 768px) {{
.phone-shell {{ padding: 15px 15px 12px; }}
.dot {{ width: 6px; height: 6px; }}
.dot.zero {{ width: 9px; height: 9px; border-width: 2px; }}
.dot-box {{ gap: 2px; padding: 4px; }}
.key {{ font-size: 1.8rem; padding: 10px 0; }}
.key.special {{ font-size: 1.5rem; }}
.dot-number {{ font-size: 1.1rem; height: 1.4rem; line-height: 1.4rem; }}
.dots-scroll {{ gap: 4px; padding: 6px 4px; }}
.screen {{ padding: 12px 10px 8px; }}
.card-grid {{ gap: 6px; }}
.student-card .card-name {{ font-size: 0.75rem; }}
.student-card .card-id {{ font-size: 1.1rem; }}
}}
</style>
</head>
<body>
<!-- ================= 视图 1:学号卡片矩阵 ================= -->
<div class="view active" id="viewGrid">
<div class="card-grid-wrapper">
<div class="grid-title">📋 大3班《破译电话号码》选一个学号破译吧</div>
<div class="card-grid" id="cardGrid">
<!-- 动态渲染卡片 -->
</div>
</div>
</div>
<!-- ================= 视图 2:手机破译界面 ================= -->
<div class="view" id="viewPhone">
<div class="phone-shell">
<div class="status-bar">
<div class="time">📞 破译</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span class="student-badge" id="studentBadge">小朋友</span>
<button class="next-btn" onclick="backToGrid()">返回</button>
</div>
</div>
<div class="screen">
<div class="screen-header">
<div class="phone-label" id="phoneLabel">第1个号码</div>
<div class="identity-tag">
<span id="identityEmoji">👩</span>
<span id="identityName">妈妈</span>
</div>
</div>
<div class="dots-scroll" id="dotsContainer">
<!-- 动态渲染点阵 -->
</div>
<div class="hint-text" id="hintText">请观察有几个点,在下面按数字哦!</div>
</div>
<div class="keypad" id="keypad">
<!-- 动态渲染键盘 -->
</div>
</div>
</div>
<!-- 拨号弹窗 -->
<div class="modal" id="callModal">
<div class="modal-content">
<div class="avatar" id="modalAvatar">📞</div>
<div class="speech-bubble" id="modalSpeech">正在拨号...</div>
<button class="btn-next" id="modalBtn" onclick="closeModal()" style="display: none;">听到啦,继续</button>
</div>
</div>
<script>
// ------------------------------------------------------------------
// 学生数据(由 Python 注入)
// ------------------------------------------------------------------
const studentData = {js_student_data};
// 家人头像库
const avatars = {{
"妈妈": "👩", "爸爸": "👨", "奶奶": "👵", "爷爷": "👴",
"外婆": "👵", "外公": "👴", "叔叔": "🧑", "阿姨": "👩",
"舅舅": "🧑", "姑姑": "👩"
}};
// 语音台词
const speechLines = [
"{{name}}宝贝,我是{{identity}},宝贝在幼儿园开心吗?",
"{{name}},我是{{identity}}呀,今天有没有乖乖吃饭呀?",
"喂,{{name}}宝贝,我是{{identity}},今天和小朋友玩得开心吗?",
"{{name}},我是{{identity}},今天学了什么新本领呀?",
"宝贝{{name}},我是{{identity}},下班我就来接你啦!"
];
// DOM 元素
const viewGrid = document.getElementById('viewGrid');
const viewPhone = document.getElementById('viewPhone');
const cardGrid = document.getElementById('cardGrid');
const studentBadge = document.getElementById('studentBadge');
const phoneLabel = document.getElementById('phoneLabel');
const identityEmoji = document.getElementById('identityEmoji');
const identityName = document.getElementById('identityName');
const dotsContainer = document.getElementById('dotsContainer');
const hintText = document.getElementById('hintText');
const keypad = document.getElementById('keypad');
const callModal = document.getElementById('callModal');
const modalAvatar = document.getElementById('modalAvatar');
const modalSpeech = document.getElementById('modalSpeech');
const modalBtn = document.getElementById('modalBtn');
// 状态变量
let currentStudent = null;
let currentPhoneIndex = 1;
let currentPhoneStr = "";
let currentIdentity = "";
let currentDotIndex = 0;
let isWaiting = false;
let audioCtx = null;
// 🌟 新增:用于记录本次打开网页后,已完成的学号(不再使用 localStorage)
let completedIds = new Set();
// ---------- 初始化 ----------
function initGame() {{
// 生成键盘
renderKeypad();
// 生成卡片矩阵
renderCardGrid();
try {{
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}} catch (e) {{ /* ignore */ }}
}}
// ---------- 生成 9x4 学号卡片矩阵 ----------
function renderCardGrid() {{
cardGrid.innerHTML = '';
studentData.forEach((student, index) => {{
const card = document.createElement('div');
card.className = 'student-card';
card.dataset.id = student.id;
card.id = `card-${{student.id}}`;
card.innerHTML = `
<div class="card-name">${{student.name}}</div>
<div class="card-id">${{student.id}}</div>
`;
// 检查是否在本次打开记录中已完成
if (completedIds.has(student.id)) {{
card.classList.add('done');
}}
card.addEventListener('click', () => {{
if (card.classList.contains('done')) return;
enterStudent(student);
}});
cardGrid.appendChild(card);
}});
}}
// ---------- 进入某个学生的破译界面 ----------
function enterStudent(student) {{
currentStudent = student;
// 切换视图
viewGrid.classList.remove('active');
viewPhone.classList.add('active');
// 更新顶部信息
studentBadge.innerText = `${{student.name}} · ${{student.id}}`;
// 开始第一个电话
startPhone(1);
}}
// ---------- 返回卡片矩阵 ----------
function backToGrid() {{
viewPhone.classList.remove('active');
viewGrid.classList.add('active');
// 取消语音
if ('speechSynthesis' in window) window.speechSynthesis.cancel();
// 重新渲染卡片(刷新置灰状态)
renderCardGrid();
}}
// ---------- 生成 3x4 键盘 ----------
function renderKeypad() {{
keypad.innerHTML = '';
const keys = [
{{ label: '1', value: 1, type: 'number' }}, {{ label: '2', value: 2, type: 'number' }}, {{ label: '3', value: 3, type: 'number' }},
{{ label: '4', value: 4, type: 'number' }}, {{ label: '5', value: 5, type: 'number' }}, {{ label: '6', value: 6, type: 'number' }},
{{ label: '7', value: 7, type: 'number' }}, {{ label: '8', value: 8, type: 'number' }}, {{ label: '9', value: 9, type: 'number' }},
{{ label: '*', value: '*', type: 'special' }}, {{ label: '0', value: 0, type: 'number' }}, {{ label: '#', value: '#', type: 'special' }}
];
keys.forEach(item => {{
const btn = document.createElement('button');
btn.className = 'key' + (item.type === 'special' ? ' special' : '');
btn.innerText = item.label;
btn.addEventListener('click', () => handleKeyPress(item.value));
keypad.appendChild(btn);
}});
}}
// ---------- 开始某个电话 ----------
function startPhone(index) {{
currentPhoneIndex = index;
currentDotIndex = 0;
isWaiting = false;
if (index === 1) {{
currentPhoneStr = currentStudent.phone1 || '';
currentIdentity = currentStudent.id1 || '家人';
}} else {{
currentPhoneStr = currentStudent.phone2 || '';
currentIdentity = currentStudent.id2 || '家人';
}}
if (!currentPhoneStr) {{
if (index === 1 && currentStudent.phone2) {{
startPhone(2);
}} else {{
completeStudent();
}}
return;
}}
phoneLabel.innerText = index === 1 ? '第1个号码' : '第2个号码';
identityName.innerText = currentIdentity;
identityEmoji.innerText = avatars[currentIdentity] || '👤';
hintText.innerText = '请观察有几个点,在下面按数字哦!';
hintText.style.color = '#ff6b6b';
renderDots();
updateActiveDot();
}}
// ---------- 渲染点阵 ----------
function renderDots() {{
dotsContainer.innerHTML = '';
const digits = currentPhoneStr.split('');
digits.forEach((digit, index) => {{
const group = document.createElement('div');
group.className = 'dot-group';
group.id = `dot-group-${{index}}`;
const box = document.createElement('div');
box.className = 'dot-box';
let num = parseInt(digit, 10);
if (isNaN(num)) num = 0;
if (num === 0) {{
const dot = document.createElement('div');
dot.className = 'dot zero';
box.appendChild(dot);
}} else {{
for (let i = 0; i < num; i++) {{
const dot = document.createElement('div');
dot.className = 'dot';
box.appendChild(dot);
}}
}}
const numText = document.createElement('div');
numText.className = 'dot-number';
numText.innerText = digit;
group.appendChild(box);
group.appendChild(numText);
dotsContainer.appendChild(group);
}});
}}
// ---------- 高亮当前点组 ----------
function updateActiveDot() {{
document.querySelectorAll('.dot-group').forEach(el => el.classList.remove('active'));
if (currentDotIndex < currentPhoneStr.length) {{
const currentGroup = document.getElementById(`dot-group-${{currentDotIndex}}`);
if (currentGroup) {{
currentGroup.classList.add('active');
}}
}}
}}
// ---------- 处理按键 ----------
function handleKeyPress(value) {{
if (isWaiting) return;
if (value === '*' || value === '#') return;
const targetDigit = parseInt(currentPhoneStr[currentDotIndex], 10);
if (value === targetDigit) {{
playSound('correct');
const group = document.getElementById(`dot-group-${{currentDotIndex}}`);
if (group) {{
group.querySelector('.dot-number').classList.add('show');
}}
currentDotIndex++;
if (currentDotIndex < currentPhoneStr.length) {{
updateActiveDot();
}} else {{
isWaiting = true;
setTimeout(() => dialNumber(), 800);
}}
}} else {{
playSound('wrong');
hintText.innerText = '哎呀,不对哦!再数一数,试一次吧!';
hintText.style.color = '#ef476f';
const activeBox = document.querySelector('.dot-group.active .dot-box');
if (activeBox) {{
activeBox.classList.add('shake');
setTimeout(() => activeBox.classList.remove('shake'), 400);
}}
}}
}}
// ---------- 拨号动画 ----------
function dialNumber() {{
callModal.classList.add('show');
modalAvatar.innerText = '📞';
modalSpeech.innerText = '正在拨号...';
modalBtn.style.display = 'none';
playSound('dial');
setTimeout(() => {{
playSound('connect');
modalAvatar.innerText = avatars[currentIdentity] || '👤';
const line = speechLines[Math.floor(Math.random() * speechLines.length)]
.replace(/{{name}}/g, currentStudent.name)
.replace(/{{identity}}/g, currentIdentity);
modalSpeech.innerText = line;
speakText(line);
modalBtn.innerText = currentPhoneIndex === 1 ? '听完了,打第二个电话' : '好啦,完成';
modalBtn.style.display = 'inline-block';
}}, 2000);
}}
// ---------- 关闭弹窗 ----------
function closeModal() {{
callModal.classList.remove('show');
if ('speechSynthesis' in window) {{
window.speechSynthesis.cancel();
}}
if (currentPhoneIndex === 1) {{
startPhone(2);
}} else {{
completeStudent();
}}
}}
// ---------- 完成某个学生,返回卡片矩阵并置灰 ----------
function completeStudent() {{
if (currentStudent) {{
// 🌟 记录到内存中的 Set 集合里,不再使用 localStorage
completedIds.add(currentStudent.id);
}}
// 返回卡片矩阵
backToGrid();
}}
// ---------- 音效 ----------
function playSound(type) {{
if (!audioCtx) return;
if (audioCtx.state === 'suspended') audioCtx.resume();
const now = audioCtx.currentTime;
if (type === 'correct') {{
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.type = 'sine';
osc.frequency.setValueAtTime(880, now);
osc.frequency.exponentialRampToValueAtTime(1318.51, now + 0.1);
gain.gain.setValueAtTime(0.3, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.2);
osc.start(now);
osc.stop(now + 0.2);
}} else if (type === 'wrong') {{
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(150, now);
osc.frequency.linearRampToValueAtTime(100, now + 0.2);
gain.gain.setValueAtTime(0.3, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
osc.start(now);
osc.stop(now + 0.3);
}} else if (type === 'dial') {{
const osc1 = audioCtx.createOscillator();
const gain1 = audioCtx.createGain();
osc1.connect(gain1);
gain1.connect(audioCtx.destination);
osc1.type = 'sine';
osc1.frequency.setValueAtTime(440, now);
gain1.gain.setValueAtTime(0.1, now);
osc1.start(now);
osc1.stop(now + 0.5);
setTimeout(() => {{
const osc2 = audioCtx.createOscillator();
const gain2 = audioCtx.createGain();
osc2.connect(gain2);
gain2.connect(audioCtx.destination);
osc2.type = 'sine';
osc2.frequency.setValueAtTime(440, audioCtx.currentTime);
gain2.gain.setValueAtTime(0.1, audioCtx.currentTime);
osc2.start(audioCtx.currentTime);
osc2.stop(audioCtx.currentTime + 0.5);
}}, 1000);
}} else if (type === 'connect') {{
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.type = 'triangle';
osc.frequency.setValueAtTime(523.25, now);
gain.gain.setValueAtTime(0.2, now);
osc.start(now);
osc.stop(now + 0.1);
setTimeout(() => {{
const osc2 = audioCtx.createOscillator();
const gain2 = audioCtx.createGain();
osc2.connect(gain2);
gain2.connect(audioCtx.destination);
osc2.type = 'triangle';
osc2.frequency.setValueAtTime(659.25, audioCtx.currentTime);
gain2.gain.setValueAtTime(0.2, audioCtx.currentTime);
osc2.start(audioCtx.currentTime);
osc2.stop(audioCtx.currentTime + 0.15);
}}, 120);
}}
}}
// ---------- 语音合成 ----------
function speakText(text) {{
if ('speechSynthesis' in window) {{
window.speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = 'zh-CN';
utterance.rate = 0.9;
utterance.pitch = 1.2;
const voices = window.speechSynthesis.getVoices();
const zhVoice = voices.find(v => v.lang.includes('zh'));
if (zhVoice) utterance.voice = zhVoice;
window.speechSynthesis.speak(utterance);
}}
}}
// 启动
window.onload = initGame;
</script>
</body>
</html>'''
# 3. 保存并输出
try:
os.makedirs(os.path.dirname(save_html_path), exist_ok=True)
with open(save_html_path, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f'✅ 破译电话号码游戏已成功生成!')
print(f'📁 文件保存位置: {save_html_path}')
except Exception as e:
print(f'❌ 生成 HTML 文件失败: {e}')
if __name__ == '__main__':
# ================= 配置区 =================
path = r'D:\000Python网页html\20261026破译电话号码'
EXCEL_PATH = path + r'\家长手机号码.xlsx'
SAVE_PATH = path + r'\006破译电话号码_自动重置版.html'
# =========================================
generate_phone_crack_game(EXCEL_PATH, SAVE_PATH)

只显示33个牌子

四、制作真手机号
从孩子通里面下载真手机号



把整页复制。

每个孩子的家长手机号数量不同(1-6个之间)

需要手动修改,太费时了。(也可以考虑用Python删除空行,删除已激活等文字。)
我还是用虚假号码。


python
'''
36个学号卡、宽屏手机(11个数字横排)
重新打开还是白色学号
不同数量的手机号
deepseek,阿夏
20261027
'''
import os
import pandas as pd
def generate_phone_crack_game(excel_path, save_html_path):
"""
读取 Excel 数据,生成《破译电话号码》的 HTML 游戏文件。
支持:每个学号 1 个到多个号码不等,自动依次拨号。
(每次刷新/重新打开页面,状态自动重置为全白)
"""
print(f'正在读取 Excel 文件: {excel_path}')
# 1. 读取 Excel 数据
if not os.path.exists(excel_path):
print(f'❌ 找不到 Excel 文件: {excel_path}')
return
try:
df = pd.read_excel(excel_path, dtype=str)
df = df.fillna('')
student_list = []
for _, row in df.iterrows():
s_id = str(row.get('学号', '')).strip()
s_name = str(row.get('姓名', '')).strip()
if not s_id and not s_name:
continue
# 动态提取所有的手机号码和对应的身份
# 遍历表格列名,找出所有的 "手机号码X"
phones = []
for col in df.columns:
if str(col).startswith('手机号码'):
phone = str(row.get(col, '')).strip()
# 清理可能存在的 .0 后缀
if phone.endswith('.0'):
phone = phone[:-2]
# 只有号码不为空时才添加
if phone:
# 获取对应的身份列名(把 "手机号码" 替换为 "身份")
identity_col = str(col).replace('手机号码', '身份')
identity = str(row.get(identity_col, '')).strip()
if not identity:
identity = '家人'
phones.append(f'{{ identity: "{identity}", phone: "{phone}" }}')
# 将这个学生的所有号码打包成数组
phones_js = "[\n " + ",\n ".join(phones) + "\n ]"
item_str = f'{{ id: "{s_id}", name: "{s_name}", phones: {phones_js} }}'
student_list.append(item_str)
js_student_data = "[\n " + ",\n ".join(student_list) + "\n ]"
print(f'成功解析 {len(student_list)} 名幼儿的数据。')
except Exception as e:
print(f'❌ 读取 Excel 失败: {e}')
return
# 2. 构造 HTML 内容
html_content = f'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>破译电话号码</title>
<style>
* {{
box-sizing: border-box;
user-select: none;
-webkit-tap-highlight-color: transparent;
margin: 0;
padding: 0;
}}
body {{
font-family: 'Comic Sans MS', 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
touch-action: none;
padding: 10px;
}}
/* ====== 视图容器 ====== */
.view {{
display: none;
width: 100%;
height: 100%;
max-width: 800px;
max-height: 850px;
flex-direction: column;
align-items: center;
justify-content: center;
}}
.view.active {{
display: flex;
}}
/* ====== 卡片矩阵视图 ====== */
.card-grid-wrapper {{
width: 100%;
height: 100%;
max-height: 850px;
background: #f1f3f5;
border-radius: 40px;
box-shadow: 0 30px 40px rgba(0,0,0,0.2), inset 0 0 0 8px #2d2d2d, inset 0 0 0 12px #5c5c5c;
padding: 24px 20px;
display: flex;
flex-direction: column;
overflow: hidden;
border: 2px solid #aaa;
}}
.grid-title {{
font-size: 1.6rem;
font-weight: bold;
color: #073b4c;
text-align: center;
margin-bottom: 16px;
background: #ffd166;
padding: 10px 0;
border-radius: 50px;
box-shadow: 0 4px 0 #c79a3e;
flex-shrink: 0;
}}
.card-grid {{
flex: 1;
display: grid;
grid-template-columns: repeat(9, 1fr);
grid-template-rows: repeat(4, 1fr);
gap: 10px;
overflow-y: auto;
padding: 6px;
background: #e9ecef;
border-radius: 20px;
align-content: start;
}}
.student-card {{
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: #ffffff;
border-radius: 14px;
font-size: 1rem;
font-weight: bold;
color: #118ab2;
box-shadow: 0 4px 0 #cbd5e1, 0 2px 6px rgba(0,0,0,0.08);
cursor: pointer;
transition: all 0.15s;
border: 2px solid transparent;
padding: 4px;
text-align: center;
overflow: hidden;
}}
.student-card .card-name {{
font-size: 1rem;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}}
.student-card .card-id {{
font-size: 1.6rem;
color: #666;
font-weight: 900;
margin-top: 2px;
}}
.student-card:active {{
transform: translateY(4px);
box-shadow: 0 0 0 #cbd5e1, 0 2px 6px rgba(0,0,0,0.08);
}}
/* 已完成的卡片置灰 */
.student-card.done {{
background: #ced4da;
color: #888;
box-shadow: 0 2px 0 #adb5bd;
pointer-events: none;
opacity: 0.7;
border-color: #adb5bd;
}}
.student-card.done .card-name {{
color: #666;
}}
.student-card.done .card-id {{
color: #999;
}}
/* ====== 手机视图 ====== */
.phone-shell {{
width: 100%;
height: 100%;
max-height: 850px;
background: #f1f3f5;
border-radius: 48px;
box-shadow: 0 30px 40px rgba(0,0,0,0.2), inset 0 0 0 8px #2d2d2d, inset 0 0 0 12px #5c5c5c;
padding: 20px 22px 18px;
display: flex;
flex-direction: column;
transition: all 0.2s;
overflow: hidden;
border: 2px solid #aaa;
position: relative;
}}
.status-bar {{
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 8px 8px;
font-size: 0.9rem;
color: #3a3a3a;
font-weight: bold;
background: transparent;
flex-shrink: 0;
}}
.status-bar .time {{
font-size: 1.2rem;
}}
.status-bar .student-badge {{
background: #ffb703;
padding: 6px 16px;
border-radius: 40px;
font-size: 1rem;
color: #fff;
box-shadow: 0 2px 0 #c77d00;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}}
.screen {{
flex: 1;
background: #ffffff;
border-radius: 28px;
padding: 16px 18px 10px;
box-shadow: inset 0 0 0 2px #e0e0e0, 0 6px 0 #b0b0b0;
display: flex;
flex-direction: column;
overflow: hidden;
margin-bottom: 12px;
min-height: 0;
}}
.screen-header {{
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 4px 12px;
font-size: 1.1rem;
font-weight: bold;
color: #118ab2;
flex-shrink: 0;
}}
.screen-header .phone-label {{
background: #e0f2fe;
padding: 8px 18px;
border-radius: 30px;
}}
.screen-header .identity-tag {{
background: #ffd166;
padding: 8px 20px;
border-radius: 30px;
color: #333;
display: flex;
align-items: center;
gap: 6px;
}}
.dots-scroll {{
flex: 1;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
justify-content: space-between;
gap: 6px;
padding: 10px 6px;
background: #f8fafc;
border-radius: 20px;
border: 2px dashed #cfd8dc;
overflow: hidden;
min-height: 0;
}}
.dot-group {{
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
max-width: 60px;
min-width: 0;
transition: all 0.2s ease;
}}
.dot-box {{
display: flex;
flex-wrap: wrap;
justify-content: center;
align-content: center;
gap: 3px;
background: #fff;
padding: 6px;
border-radius: 12px;
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
width: 100%;
aspect-ratio: 1 / 1;
max-width: 52px;
border: 2px solid transparent;
transition: 0.2s;
}}
.dot-group.active .dot-box {{
border: 3px solid #06d6a0;
transform: scale(1.1);
box-shadow: 0 0 12px rgba(6, 214, 160, 0.5);
z-index: 2;
}}
.dot {{
width: 8px;
height: 8px;
background-color: #ef476f;
border-radius: 50%;
display: inline-block;
}}
.dot.zero {{
background-color: transparent;
border: 3px solid #ef476f;
width: 12px;
height: 12px;
}}
.dot-number {{
margin-top: 6px;
font-size: 1.4rem;
font-weight: 800;
color: #073b4c;
height: 1.6rem;
line-height: 1.6rem;
text-align: center;
opacity: 0;
transform: scale(0.5);
transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}}
.dot-number.show {{
opacity: 1;
transform: scale(1);
}}
.hint-text {{
font-size: 1.1rem;
color: #ff6b6b;
text-align: center;
margin-top: 10px;
height: 26px;
font-weight: bold;
flex-shrink: 0;
}}
.keypad {{
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px 16px;
padding: 6px 8px 0;
flex-shrink: 0;
}}
.key {{
background: #ffffff;
border: none;
border-radius: 20px;
font-size: 2.4rem;
font-weight: 700;
color: #118ab2;
padding: 14px 0;
cursor: pointer;
box-shadow: 0 6px 0 #cbd5e1, 0 4px 10px rgba(0,0,0,0.05);
transition: all 0.08s;
text-align: center;
line-height: 1;
font-family: inherit;
border: 1px solid #e2e8f0;
}}
.key:active {{
transform: translateY(6px);
box-shadow: 0 0 0 #cbd5e1, 0 4px 10px rgba(0,0,0,0.05);
}}
.key.special {{
background: #e2e8f0;
color: #4a5568;
font-size: 2rem;
box-shadow: 0 6px 0 #a0aec0, 0 4px 10px rgba(0,0,0,0.05);
}}
.key.special:active {{
box-shadow: 0 0 0 #a0aec0, 0 4px 10px rgba(0,0,0,0.05);
}}
.next-btn {{
background: #06d6a0;
border: none;
border-radius: 40px;
color: #fff;
font-weight: bold;
font-size: 1.1rem;
padding: 10px 20px;
box-shadow: 0 4px 0 #048a67;
cursor: pointer;
transition: 0.08s;
font-family: inherit;
white-space: nowrap;
margin-left: 10px;
}}
.next-btn:active {{
transform: translateY(4px);
box-shadow: 0 0 0 #048a67;
}}
/* 弹窗 */
.modal {{
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.6);
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s;
z-index: 100;
padding: 20px;
}}
.modal.show {{
opacity: 1;
pointer-events: auto;
}}
.modal-content {{
background: #fff;
border-radius: 40px;
padding: 40px 30px;
text-align: center;
max-width: 420px;
width: 100%;
transform: scale(0.85);
transition: transform 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
box-shadow: 0 20px 30px rgba(0,0,0,0.2);
}}
.modal.show .modal-content {{
transform: scale(1);
}}
.avatar {{
font-size: 6rem;
margin-bottom: 16px;
display: inline-block;
animation: bounce 2s infinite;
}}
@keyframes bounce {{
0%, 100% {{ transform: translateY(0); }}
50% {{ transform: translateY(-12px); }}
}}
.speech-bubble {{
background: #e0f7fa;
border-radius: 24px;
padding: 20px;
font-size: 1.3rem;
color: #006064;
position: relative;
margin-bottom: 26px;
border: 3px solid #4dd0e1;
line-height: 1.5;
word-break: break-word;
}}
.speech-bubble::before {{
content: '';
position: absolute;
top: -18px;
left: 50%;
transform: translateX(-50%);
border-width: 0 16px 16px 16px;
border-style: solid;
border-color: transparent transparent #4dd0e1 transparent;
}}
.btn-next {{
background: #06d6a0;
color: white;
border: none;
padding: 16px 40px;
font-size: 1.3rem;
border-radius: 50px;
cursor: pointer;
box-shadow: 0 6px 0 #048a67;
font-weight: bold;
font-family: inherit;
transition: 0.08s;
}}
.btn-next:active {{
transform: translateY(6px);
box-shadow: 0 0 0 #048a67;
}}
@keyframes shake {{
0%, 100% {{ transform: translateX(0); }}
25% {{ transform: translateX(-8px); }}
50% {{ transform: translateX(8px); }}
75% {{ transform: translateX(-8px); }}
}}
.shake {{
animation: shake 0.35s;
}}
/* 小屏适配 */
@media (max-width: 768px) {{
.phone-shell {{ padding: 15px 15px 12px; }}
.dot {{ width: 6px; height: 6px; }}
.dot.zero {{ width: 9px; height: 9px; border-width: 2px; }}
.dot-box {{ gap: 2px; padding: 4px; }}
.key {{ font-size: 1.8rem; padding: 10px 0; }}
.key.special {{ font-size: 1.5rem; }}
.dot-number {{ font-size: 1.1rem; height: 1.4rem; line-height: 1.4rem; }}
.dots-scroll {{ gap: 4px; padding: 6px 4px; }}
.screen {{ padding: 12px 10px 8px; }}
.card-grid {{ gap: 6px; }}
.student-card .card-name {{ font-size: 0.75rem; }}
.student-card .card-id {{ font-size: 1.1rem; }}
}}
</style>
</head>
<body>
<!-- ================= 视图 1:学号卡片矩阵 ================= -->
<div class="view active" id="viewGrid">
<div class="card-grid-wrapper">
<div class="grid-title">📋 大3班《破译电话号码》选一个学号破译吧</div>
<div class="card-grid" id="cardGrid">
<!-- 动态渲染卡片 -->
</div>
</div>
</div>
<!-- ================= 视图 2:手机破译界面 ================= -->
<div class="view" id="viewPhone">
<div class="phone-shell">
<div class="status-bar">
<div class="time">📞 破译</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span class="student-badge" id="studentBadge">小朋友</span>
<button class="next-btn" onclick="backToGrid()">返回</button>
</div>
</div>
<div class="screen">
<div class="screen-header">
<div class="phone-label" id="phoneLabel">第1个号码</div>
<div class="identity-tag">
<span id="identityEmoji">👩</span>
<span id="identityName">妈妈</span>
</div>
</div>
<div class="dots-scroll" id="dotsContainer">
<!-- 动态渲染点阵 -->
</div>
<div class="hint-text" id="hintText">请观察有几个点,在下面按数字哦!</div>
</div>
<div class="keypad" id="keypad">
<!-- 动态渲染键盘 -->
</div>
</div>
</div>
<!-- 拨号弹窗 -->
<div class="modal" id="callModal">
<div class="modal-content">
<div class="avatar" id="modalAvatar">📞</div>
<div class="speech-bubble" id="modalSpeech">正在拨号...</div>
<button class="btn-next" id="modalBtn" onclick="closeModal()" style="display: none;">听到啦,继续</button>
</div>
</div>
<script>
// ------------------------------------------------------------------
// 学生数据(由 Python 注入,包含了动态的 phones 数组)
// ------------------------------------------------------------------
const studentData = {js_student_data};
// 家人头像库
const avatars = {{
"妈妈": "👩", "爸爸": "👨", "奶奶": "👵", "爷爷": "👴",
"外婆": "👵", "外公": "👴", "叔叔": "🧑", "阿姨": "👩",
"舅舅": "🧑", "姑姑": "👩"
}};
// 语音台词
const speechLines = [
"{{name}}宝贝,我是{{identity}},宝贝在幼儿园开心吗?",
"{{name}},我是{{identity}}呀,今天有没有乖乖吃饭呀?",
"喂,{{name}}宝贝,我是{{identity}},今天和小朋友玩得开心吗?",
"{{name}},我是{{identity}},今天学了什么新本领呀?",
"宝贝{{name}},我是{{identity}},下班我就来接你啦!"
];
// DOM 元素
const viewGrid = document.getElementById('viewGrid');
const viewPhone = document.getElementById('viewPhone');
const cardGrid = document.getElementById('cardGrid');
const studentBadge = document.getElementById('studentBadge');
const phoneLabel = document.getElementById('phoneLabel');
const identityEmoji = document.getElementById('identityEmoji');
const identityName = document.getElementById('identityName');
const dotsContainer = document.getElementById('dotsContainer');
const hintText = document.getElementById('hintText');
const keypad = document.getElementById('keypad');
const callModal = document.getElementById('callModal');
const modalAvatar = document.getElementById('modalAvatar');
const modalSpeech = document.getElementById('modalSpeech');
const modalBtn = document.getElementById('modalBtn');
// 状态变量
let currentStudent = null;
let currentPhoneIdx = 0; // 当前拨打的是第几个号码(索引)
let currentPhoneStr = "";
let currentIdentity = "";
let currentDotIndex = 0;
let isWaiting = false;
let audioCtx = null;
// 用于记录本次打开网页后,已完成的学号
let completedIds = new Set();
// ---------- 初始化 ----------
function initGame() {{
renderKeypad();
renderCardGrid();
try {{
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}} catch (e) {{ /* ignore */ }}
}}
// ---------- 生成 9x4 学号卡片矩阵 ----------
function renderCardGrid() {{
cardGrid.innerHTML = '';
studentData.forEach((student, index) => {{
const card = document.createElement('div');
card.className = 'student-card';
card.dataset.id = student.id;
card.id = `card-${{student.id}}`;
card.innerHTML = `
<div class="card-name">${{student.name}}</div>
<div class="card-id">${{student.id}}</div>
`;
if (completedIds.has(student.id)) {{
card.classList.add('done');
}}
card.addEventListener('click', () => {{
if (card.classList.contains('done')) return;
enterStudent(student);
}});
cardGrid.appendChild(card);
}});
}}
// ---------- 进入某个学生的破译界面 ----------
function enterStudent(student) {{
currentStudent = student;
viewGrid.classList.remove('active');
viewPhone.classList.add('active');
studentBadge.innerText = `${{student.name}} · ${{student.id}}`;
startPhone(0); // 从第 0 个号码开始
}}
// ---------- 返回卡片矩阵 ----------
function backToGrid() {{
viewPhone.classList.remove('active');
viewGrid.classList.add('active');
if ('speechSynthesis' in window) window.speechSynthesis.cancel();
renderCardGrid();
}}
// ---------- 生成 3x4 键盘 ----------
function renderKeypad() {{
keypad.innerHTML = '';
const keys = [
{{ label: '1', value: 1, type: 'number' }}, {{ label: '2', value: 2, type: 'number' }}, {{ label: '3', value: 3, type: 'number' }},
{{ label: '4', value: 4, type: 'number' }}, {{ label: '5', value: 5, type: 'number' }}, {{ label: '6', value: 6, type: 'number' }},
{{ label: '7', value: 7, type: 'number' }}, {{ label: '8', value: 8, type: 'number' }}, {{ label: '9', value: 9, type: 'number' }},
{{ label: '*', value: '*', type: 'special' }}, {{ label: '0', value: 0, type: 'number' }}, {{ label: '#', value: '#', type: 'special' }}
];
keys.forEach(item => {{
const btn = document.createElement('button');
btn.className = 'key' + (item.type === 'special' ? ' special' : '');
btn.innerText = item.label;
btn.addEventListener('click', () => handleKeyPress(item.value));
keypad.appendChild(btn);
}});
}}
// ---------- 开始某个电话 ----------
function startPhone(phoneIdx) {{
currentPhoneIdx = phoneIdx;
currentDotIndex = 0;
isWaiting = false;
// 动态从 phones 数组中获取对应的号码和身份
const phoneData = currentStudent.phones[phoneIdx];
currentPhoneStr = phoneData.phone;
currentIdentity = phoneData.identity;
// 如果这个号码是空的(预防性处理,通常 Python 端已过滤)
if (!currentPhoneStr) {{
goToNextPhone();
return;
}}
// 更新界面标题
phoneLabel.innerText = `第${{phoneIdx + 1}}个号码`;
identityName.innerText = currentIdentity;
identityEmoji.innerText = avatars[currentIdentity] || '👤';
hintText.innerText = '请观察有几个点,在下面按数字哦!';
hintText.style.color = '#ff6b6b';
renderDots();
updateActiveDot();
}}
// ---------- 渲染点阵 ----------
function renderDots() {{
dotsContainer.innerHTML = '';
const digits = currentPhoneStr.split('');
digits.forEach((digit, index) => {{
const group = document.createElement('div');
group.className = 'dot-group';
group.id = `dot-group-${{index}}`;
const box = document.createElement('div');
box.className = 'dot-box';
let num = parseInt(digit, 10);
if (isNaN(num)) num = 0;
if (num === 0) {{
const dot = document.createElement('div');
dot.className = 'dot zero';
box.appendChild(dot);
}} else {{
for (let i = 0; i < num; i++) {{
const dot = document.createElement('div');
dot.className = 'dot';
box.appendChild(dot);
}}
}}
const numText = document.createElement('div');
numText.className = 'dot-number';
numText.innerText = digit;
group.appendChild(box);
group.appendChild(numText);
dotsContainer.appendChild(group);
}});
}}
// ---------- 高亮当前点组 ----------
function updateActiveDot() {{
document.querySelectorAll('.dot-group').forEach(el => el.classList.remove('active'));
if (currentDotIndex < currentPhoneStr.length) {{
const currentGroup = document.getElementById(`dot-group-${{currentDotIndex}}`);
if (currentGroup) {{
currentGroup.classList.add('active');
}}
}}
}}
// ---------- 处理按键 ----------
function handleKeyPress(value) {{
if (isWaiting) return;
if (value === '*' || value === '#') return;
const targetDigit = parseInt(currentPhoneStr[currentDotIndex], 10);
if (value === targetDigit) {{
playSound('correct');
const group = document.getElementById(`dot-group-${{currentDotIndex}}`);
if (group) {{
group.querySelector('.dot-number').classList.add('show');
}}
currentDotIndex++;
if (currentDotIndex < currentPhoneStr.length) {{
updateActiveDot();
}} else {{
isWaiting = true;
setTimeout(() => dialNumber(), 800);
}}
}} else {{
playSound('wrong');
hintText.innerText = '哎呀,不对哦!再数一数,试一次吧!';
hintText.style.color = '#ef476f';
const activeBox = document.querySelector('.dot-group.active .dot-box');
if (activeBox) {{
activeBox.classList.add('shake');
setTimeout(() => activeBox.classList.remove('shake'), 400);
}}
}}
}}
// ---------- 拨号动画 ----------
function dialNumber() {{
callModal.classList.add('show');
modalAvatar.innerText = '📞';
modalSpeech.innerText = '正在拨号...';
modalBtn.style.display = 'none';
playSound('dial');
setTimeout(() => {{
playSound('connect');
modalAvatar.innerText = avatars[currentIdentity] || '👤';
const line = speechLines[Math.floor(Math.random() * speechLines.length)]
.replace(/{{name}}/g, currentStudent.name)
.replace(/{{identity}}/g, currentIdentity);
modalSpeech.innerText = line;
speakText(line);
// 🌟 动态判断是否还有下一个号码
const isLastPhone = (currentPhoneIdx === currentStudent.phones.length - 1);
modalBtn.innerText = isLastPhone ? '好啦,完成' : '听完了,打下一个电话';
modalBtn.style.display = 'inline-block';
}}, 2000);
}}
// ---------- 关闭弹窗 ----------
function closeModal() {{
callModal.classList.remove('show');
if ('speechSynthesis' in window) {{
window.speechSynthesis.cancel();
}}
goToNextPhone();
}}
// ---------- 跳到下一个号码或结束 ----------
function goToNextPhone() {{
const nextIdx = currentPhoneIdx + 1;
// 如果还有号码,继续拨打
if (nextIdx < currentStudent.phones.length) {{
startPhone(nextIdx);
}} else {{
// 所有号码都拨完了,完成该学生
completeStudent();
}}
}}
// ---------- 完成某个学生,返回卡片矩阵并置灰 ----------
function completeStudent() {{
if (currentStudent) {{
completedIds.add(currentStudent.id);
}}
backToGrid();
}}
// ---------- 音效 ----------
function playSound(type) {{
if (!audioCtx) return;
if (audioCtx.state === 'suspended') audioCtx.resume();
const now = audioCtx.currentTime;
if (type === 'correct') {{
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.type = 'sine';
osc.frequency.setValueAtTime(880, now);
osc.frequency.exponentialRampToValueAtTime(1318.51, now + 0.1);
gain.gain.setValueAtTime(0.3, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.2);
osc.start(now);
osc.stop(now + 0.2);
}} else if (type === 'wrong') {{
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(150, now);
osc.frequency.linearRampToValueAtTime(100, now + 0.2);
gain.gain.setValueAtTime(0.3, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
osc.start(now);
osc.stop(now + 0.3);
}} else if (type === 'dial') {{
const osc1 = audioCtx.createOscillator();
const gain1 = audioCtx.createGain();
osc1.connect(gain1);
gain1.connect(audioCtx.destination);
osc1.type = 'sine';
osc1.frequency.setValueAtTime(440, now);
gain1.gain.setValueAtTime(0.1, now);
osc1.start(now);
osc1.stop(now + 0.5);
setTimeout(() => {{
const osc2 = audioCtx.createOscillator();
const gain2 = audioCtx.createGain();
osc2.connect(gain2);
gain2.connect(audioCtx.destination);
osc2.type = 'sine';
osc2.frequency.setValueAtTime(440, audioCtx.currentTime);
gain2.gain.setValueAtTime(0.1, audioCtx.currentTime);
osc2.start(audioCtx.currentTime);
osc2.stop(audioCtx.currentTime + 0.5);
}}, 1000);
}} else if (type === 'connect') {{
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.type = 'triangle';
osc.frequency.setValueAtTime(523.25, now);
gain.gain.setValueAtTime(0.2, now);
osc.start(now);
osc.stop(now + 0.1);
setTimeout(() => {{
const osc2 = audioCtx.createOscillator();
const gain2 = audioCtx.createGain();
osc2.connect(gain2);
gain2.connect(audioCtx.destination);
osc2.type = 'triangle';
osc2.frequency.setValueAtTime(659.25, audioCtx.currentTime);
gain2.gain.setValueAtTime(0.2, audioCtx.currentTime);
osc2.start(audioCtx.currentTime);
osc2.stop(audioCtx.currentTime + 0.15);
}}, 120);
}}
}}
// ---------- 语音合成 ----------
function speakText(text) {{
if ('speechSynthesis' in window) {{
window.speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = 'zh-CN';
utterance.rate = 0.9;
utterance.pitch = 1.2;
const voices = window.speechSynthesis.getVoices();
const zhVoice = voices.find(v => v.lang.includes('zh'));
if (zhVoice) utterance.voice = zhVoice;
window.speechSynthesis.speak(utterance);
}}
}}
// 启动
window.onload = initGame;
</script>
</body>
</html>'''
# 3. 保存并输出
try:
os.makedirs(os.path.dirname(save_html_path), exist_ok=True)
with open(save_html_path, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f'✅ 破译电话号码游戏已成功生成!')
print(f'📁 文件保存位置: {save_html_path}')
except Exception as e:
print(f'❌ 生成 HTML 文件失败: {e}')
if __name__ == '__main__':
# ================= 配置区 =================
path = r'D:\000Python网页html\20261026破译电话号码'
EXCEL_PATH = path + r'\家长手机号码.xlsx'
SAVE_PATH = path + r'\007破译电话号码_动态号码版.html'
# =========================================
generate_phone_crack_game(EXCEL_PATH, SAVE_PATH)



我需要手机页面提示一共有几个手机号

python
'''
36个学号卡、宽屏手机(11个数字横排)
重新打开还是白色学号
不同数量的手机号
手机号上显示有几条手机号
deepseek,阿夏
20261027
'''
import os
import pandas as pd
def generate_phone_crack_game(excel_path, save_html_path):
"""
读取 Excel 数据,生成《破译电话号码》的 HTML 游戏文件。
支持:每个学号 1 个到多个号码不等,自动依次拨号。
优化:顶部显示当前是第几个号码/总共几个号码。
(每次刷新/重新打开页面,状态自动重置为全白)
"""
print(f'正在读取 Excel 文件: {excel_path}')
# 1. 读取 Excel 数据
if not os.path.exists(excel_path):
print(f'❌ 找不到 Excel 文件: {excel_path}')
return
try:
df = pd.read_excel(excel_path, dtype=str)
df = df.fillna('')
student_list = []
for _, row in df.iterrows():
s_id = str(row.get('学号', '')).strip()
s_name = str(row.get('姓名', '')).strip()
if not s_id and not s_name:
continue
# 动态提取所有的手机号码和对应的身份
phones = []
for col in df.columns:
if str(col).startswith('手机号码'):
phone = str(row.get(col, '')).strip()
# 清理可能存在的 .0 后缀
if phone.endswith('.0'):
phone = phone[:-2]
# 只有号码不为空时才添加
if phone:
identity_col = str(col).replace('手机号码', '身份')
identity = str(row.get(identity_col, '')).strip()
if not identity:
identity = '家人'
phones.append(f'{{ identity: "{identity}", phone: "{phone}" }}')
# 如果这个学生一个号码都没有,可以选择跳过或者也给个空数组
phones_js = "[\n " + ",\n ".join(phones) + "\n ]"
item_str = f'{{ id: "{s_id}", name: "{s_name}", phones: {phones_js} }}'
student_list.append(item_str)
js_student_data = "[\n " + ",\n ".join(student_list) + "\n ]"
print(f'成功解析 {len(student_list)} 名幼儿的数据。')
except Exception as e:
print(f'❌ 读取 Excel 失败: {e}')
return
# 2. 构造 HTML 内容
html_content = f'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>破译电话号码</title>
<style>
* {{
box-sizing: border-box;
user-select: none;
-webkit-tap-highlight-color: transparent;
margin: 0;
padding: 0;
}}
body {{
font-family: 'Comic Sans MS', 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
touch-action: none;
padding: 10px;
}}
/* ====== 视图容器 ====== */
.view {{
display: none;
width: 100%;
height: 100%;
max-width: 800px;
max-height: 850px;
flex-direction: column;
align-items: center;
justify-content: center;
}}
.view.active {{
display: flex;
}}
/* ====== 卡片矩阵视图 ====== */
.card-grid-wrapper {{
width: 100%;
height: 100%;
max-height: 850px;
background: #f1f3f5;
border-radius: 40px;
box-shadow: 0 30px 40px rgba(0,0,0,0.2), inset 0 0 0 8px #2d2d2d, inset 0 0 0 12px #5c5c5c;
padding: 24px 20px;
display: flex;
flex-direction: column;
overflow: hidden;
border: 2px solid #aaa;
}}
.grid-title {{
font-size: 1.6rem;
font-weight: bold;
color: #073b4c;
text-align: center;
margin-bottom: 16px;
background: #ffd166;
padding: 10px 0;
border-radius: 50px;
box-shadow: 0 4px 0 #c79a3e;
flex-shrink: 0;
}}
.card-grid {{
flex: 1;
display: grid;
grid-template-columns: repeat(9, 1fr);
grid-template-rows: repeat(4, 1fr);
gap: 10px;
overflow-y: auto;
padding: 6px;
background: #e9ecef;
border-radius: 20px;
align-content: start;
}}
.student-card {{
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: #ffffff;
border-radius: 14px;
font-size: 1rem;
font-weight: bold;
color: #118ab2;
box-shadow: 0 4px 0 #cbd5e1, 0 2px 6px rgba(0,0,0,0.08);
cursor: pointer;
transition: all 0.15s;
border: 2px solid transparent;
padding: 4px;
text-align: center;
overflow: hidden;
}}
.student-card .card-name {{
font-size: 1rem;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}}
.student-card .card-id {{
font-size: 1.6rem;
color: #666;
font-weight: 900;
margin-top: 2px;
}}
.student-card:active {{
transform: translateY(4px);
box-shadow: 0 0 0 #cbd5e1, 0 2px 6px rgba(0,0,0,0.08);
}}
/* 已完成的卡片置灰 */
.student-card.done {{
background: #ced4da;
color: #888;
box-shadow: 0 2px 0 #adb5bd;
pointer-events: none;
opacity: 0.7;
border-color: #adb5bd;
}}
.student-card.done .card-name {{
color: #666;
}}
.student-card.done .card-id {{
color: #999;
}}
/* ====== 手机视图 ====== */
.phone-shell {{
width: 100%;
height: 100%;
max-height: 850px;
background: #f1f3f5;
border-radius: 48px;
box-shadow: 0 30px 40px rgba(0,0,0,0.2), inset 0 0 0 8px #2d2d2d, inset 0 0 0 12px #5c5c5c;
padding: 20px 22px 18px;
display: flex;
flex-direction: column;
transition: all 0.2s;
overflow: hidden;
border: 2px solid #aaa;
position: relative;
}}
.status-bar {{
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 8px 8px;
font-size: 0.9rem;
color: #3a3a3a;
font-weight: bold;
background: transparent;
flex-shrink: 0;
}}
.status-bar .time {{
font-size: 1.2rem;
}}
.status-bar .student-badge {{
background: #ffb703;
padding: 6px 16px;
border-radius: 40px;
font-size: 1rem;
color: #fff;
box-shadow: 0 2px 0 #c77d00;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}}
.screen {{
flex: 1;
background: #ffffff;
border-radius: 28px;
padding: 16px 18px 10px;
box-shadow: inset 0 0 0 2px #e0e0e0, 0 6px 0 #b0b0b0;
display: flex;
flex-direction: column;
overflow: hidden;
margin-bottom: 12px;
min-height: 0;
}}
.screen-header {{
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 4px 12px;
font-size: 1.1rem;
font-weight: bold;
color: #118ab2;
flex-shrink: 0;
}}
.screen-header .phone-label {{
background: #e0f2fe;
padding: 8px 18px;
border-radius: 30px;
}}
.screen-header .identity-tag {{
background: #ffd166;
padding: 8px 20px;
border-radius: 30px;
color: #333;
display: flex;
align-items: center;
gap: 6px;
}}
.dots-scroll {{
flex: 1;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
justify-content: space-between;
gap: 6px;
padding: 10px 6px;
background: #f8fafc;
border-radius: 20px;
border: 2px dashed #cfd8dc;
overflow: hidden;
min-height: 0;
}}
.dot-group {{
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
max-width: 60px;
min-width: 0;
transition: all 0.2s ease;
}}
.dot-box {{
display: flex;
flex-wrap: wrap;
justify-content: center;
align-content: center;
gap: 3px;
background: #fff;
padding: 6px;
border-radius: 12px;
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
width: 100%;
aspect-ratio: 1 / 1;
max-width: 52px;
border: 2px solid transparent;
transition: 0.2s;
}}
.dot-group.active .dot-box {{
border: 3px solid #06d6a0;
transform: scale(1.1);
box-shadow: 0 0 12px rgba(6, 214, 160, 0.5);
z-index: 2;
}}
.dot {{
width: 8px;
height: 8px;
background-color: #ef476f;
border-radius: 50%;
display: inline-block;
}}
.dot.zero {{
background-color: transparent;
border: 3px solid #ef476f;
width: 12px;
height: 12px;
}}
.dot-number {{
margin-top: 6px;
font-size: 1.4rem;
font-weight: 800;
color: #073b4c;
height: 1.6rem;
line-height: 1.6rem;
text-align: center;
opacity: 0;
transform: scale(0.5);
transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}}
.dot-number.show {{
opacity: 1;
transform: scale(1);
}}
.hint-text {{
font-size: 1.1rem;
color: #ff6b6b;
text-align: center;
margin-top: 10px;
height: 26px;
font-weight: bold;
flex-shrink: 0;
}}
.keypad {{
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px 16px;
padding: 6px 8px 0;
flex-shrink: 0;
}}
.key {{
background: #ffffff;
border: none;
border-radius: 20px;
font-size: 2.4rem;
font-weight: 700;
color: #118ab2;
padding: 14px 0;
cursor: pointer;
box-shadow: 0 6px 0 #cbd5e1, 0 4px 10px rgba(0,0,0,0.05);
transition: all 0.08s;
text-align: center;
line-height: 1;
font-family: inherit;
border: 1px solid #e2e8f0;
}}
.key:active {{
transform: translateY(6px);
box-shadow: 0 0 0 #cbd5e1, 0 4px 10px rgba(0,0,0,0.05);
}}
.key.special {{
background: #e2e8f0;
color: #4a5568;
font-size: 2rem;
box-shadow: 0 6px 0 #a0aec0, 0 4px 10px rgba(0,0,0,0.05);
}}
.key.special:active {{
box-shadow: 0 0 0 #a0aec0, 0 4px 10px rgba(0,0,0,0.05);
}}
.next-btn {{
background: #06d6a0;
border: none;
border-radius: 40px;
color: #fff;
font-weight: bold;
font-size: 1.1rem;
padding: 10px 20px;
box-shadow: 0 4px 0 #048a67;
cursor: pointer;
transition: 0.08s;
font-family: inherit;
white-space: nowrap;
margin-left: 10px;
}}
.next-btn:active {{
transform: translateY(4px);
box-shadow: 0 0 0 #048a67;
}}
/* 弹窗 */
.modal {{
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.6);
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s;
z-index: 100;
padding: 20px;
}}
.modal.show {{
opacity: 1;
pointer-events: auto;
}}
.modal-content {{
background: #fff;
border-radius: 40px;
padding: 40px 30px;
text-align: center;
max-width: 420px;
width: 100%;
transform: scale(0.85);
transition: transform 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
box-shadow: 0 20px 30px rgba(0,0,0,0.2);
}}
.modal.show .modal-content {{
transform: scale(1);
}}
.avatar {{
font-size: 6rem;
margin-bottom: 16px;
display: inline-block;
animation: bounce 2s infinite;
}}
@keyframes bounce {{
0%, 100% {{ transform: translateY(0); }}
50% {{ transform: translateY(-12px); }}
}}
.speech-bubble {{
background: #e0f7fa;
border-radius: 24px;
padding: 20px;
font-size: 1.3rem;
color: #006064;
position: relative;
margin-bottom: 26px;
border: 3px solid #4dd0e1;
line-height: 1.5;
word-break: break-word;
}}
.speech-bubble::before {{
content: '';
position: absolute;
top: -18px;
left: 50%;
transform: translateX(-50%);
border-width: 0 16px 16px 16px;
border-style: solid;
border-color: transparent transparent #4dd0e1 transparent;
}}
.btn-next {{
background: #06d6a0;
color: white;
border: none;
padding: 16px 40px;
font-size: 1.3rem;
border-radius: 50px;
cursor: pointer;
box-shadow: 0 6px 0 #048a67;
font-weight: bold;
font-family: inherit;
transition: 0.08s;
}}
.btn-next:active {{
transform: translateY(6px);
box-shadow: 0 0 0 #048a67;
}}
@keyframes shake {{
0%, 100% {{ transform: translateX(0); }}
25% {{ transform: translateX(-8px); }}
50% {{ transform: translateX(8px); }}
75% {{ transform: translateX(-8px); }}
}}
.shake {{
animation: shake 0.35s;
}}
/* 小屏适配 */
@media (max-width: 768px) {{
.phone-shell {{ padding: 15px 15px 12px; }}
.dot {{ width: 6px; height: 6px; }}
.dot.zero {{ width: 9px; height: 9px; border-width: 2px; }}
.dot-box {{ gap: 2px; padding: 4px; }}
.key {{ font-size: 1.8rem; padding: 10px 0; }}
.key.special {{ font-size: 1.5rem; }}
.dot-number {{ font-size: 1.1rem; height: 1.4rem; line-height: 1.4rem; }}
.dots-scroll {{ gap: 4px; padding: 6px 4px; }}
.screen {{ padding: 12px 10px 8px; }}
.card-grid {{ gap: 6px; }}
.student-card .card-name {{ font-size: 0.75rem; }}
.student-card .card-id {{ font-size: 1.1rem; }}
}}
</style>
</head>
<body>
<!-- ================= 视图 1:学号卡片矩阵 ================= -->
<div class="view active" id="viewGrid">
<div class="card-grid-wrapper">
<div class="grid-title">📋 大3班《破译电话号码》选一个学号破译吧</div>
<div class="card-grid" id="cardGrid">
<!-- 动态渲染卡片 -->
</div>
</div>
</div>
<!-- ================= 视图 2:手机破译界面 ================= -->
<div class="view" id="viewPhone">
<div class="phone-shell">
<div class="status-bar">
<div class="time">📞 破译</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span class="student-badge" id="studentBadge">小朋友</span>
<button class="next-btn" onclick="backToGrid()">返回</button>
</div>
</div>
<div class="screen">
<div class="screen-header">
<div class="phone-label" id="phoneLabel">第1个号码</div>
<div class="identity-tag">
<span id="identityEmoji">👩</span>
<span id="identityName">妈妈</span>
</div>
</div>
<div class="dots-scroll" id="dotsContainer">
<!-- 动态渲染点阵 -->
</div>
<div class="hint-text" id="hintText">请观察有几个点,在下面按数字哦!</div>
</div>
<div class="keypad" id="keypad">
<!-- 动态渲染键盘 -->
</div>
</div>
</div>
<!-- 拨号弹窗 -->
<div class="modal" id="callModal">
<div class="modal-content">
<div class="avatar" id="modalAvatar">📞</div>
<div class="speech-bubble" id="modalSpeech">正在拨号...</div>
<button class="btn-next" id="modalBtn" onclick="closeModal()" style="display: none;">听到啦,继续</button>
</div>
</div>
<script>
// ------------------------------------------------------------------
// 学生数据(由 Python 注入,包含了动态的 phones 数组)
// ------------------------------------------------------------------
const studentData = {js_student_data};
// 家人头像库
const avatars = {{
"妈妈": "👩", "爸爸": "👨", "奶奶": "👵", "爷爷": "👴",
"外婆": "👵", "外公": "👴", "叔叔": "🧑", "阿姨": "👩",
"舅舅": "🧑", "姑姑": "👩"
}};
// 语音台词
const speechLines = [
"{{name}}宝贝,我是{{identity}},宝贝在幼儿园开心吗?",
"{{name}},我是{{identity}}呀,今天有没有乖乖吃饭呀?",
"喂,{{name}}宝贝,我是{{identity}},今天和小朋友玩得开心吗?",
"{{name}},我是{{identity}},今天学了什么新本领呀?",
"宝贝{{name}},我是{{identity}},下班我就来接你啦!"
];
// DOM 元素
const viewGrid = document.getElementById('viewGrid');
const viewPhone = document.getElementById('viewPhone');
const cardGrid = document.getElementById('cardGrid');
const studentBadge = document.getElementById('studentBadge');
const phoneLabel = document.getElementById('phoneLabel');
const identityEmoji = document.getElementById('identityEmoji');
const identityName = document.getElementById('identityName');
const dotsContainer = document.getElementById('dotsContainer');
const hintText = document.getElementById('hintText');
const keypad = document.getElementById('keypad');
const callModal = document.getElementById('callModal');
const modalAvatar = document.getElementById('modalAvatar');
const modalSpeech = document.getElementById('modalSpeech');
const modalBtn = document.getElementById('modalBtn');
// 状态变量
let currentStudent = null;
let currentPhoneIdx = 0; // 当前拨打的是第几个号码(索引)
let currentPhoneStr = "";
let currentIdentity = "";
let currentDotIndex = 0;
let isWaiting = false;
let audioCtx = null;
// 用于记录本次打开网页后,已完成的学号
let completedIds = new Set();
// ---------- 初始化 ----------
function initGame() {{
renderKeypad();
renderCardGrid();
try {{
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}} catch (e) {{ /* ignore */ }}
}}
// ---------- 生成 9x4 学号卡片矩阵 ----------
function renderCardGrid() {{
cardGrid.innerHTML = '';
studentData.forEach((student, index) => {{
const card = document.createElement('div');
card.className = 'student-card';
card.dataset.id = student.id;
card.id = `card-${{student.id}}`;
card.innerHTML = `
<div class="card-name">${{student.name}}</div>
<div class="card-id">${{student.id}}</div>
`;
if (completedIds.has(student.id)) {{
card.classList.add('done');
}}
card.addEventListener('click', () => {{
if (card.classList.contains('done')) return;
enterStudent(student);
}});
cardGrid.appendChild(card);
}});
}}
// ---------- 进入某个学生的破译界面 ----------
function enterStudent(student) {{
currentStudent = student;
viewGrid.classList.remove('active');
viewPhone.classList.add('active');
studentBadge.innerText = `${{student.name}} · ${{student.id}}`;
startPhone(0); // 从第 0 个号码开始
}}
// ---------- 返回卡片矩阵 ----------
function backToGrid() {{
viewPhone.classList.remove('active');
viewGrid.classList.add('active');
if ('speechSynthesis' in window) window.speechSynthesis.cancel();
renderCardGrid();
}}
// ---------- 生成 3x4 键盘 ----------
function renderKeypad() {{
keypad.innerHTML = '';
const keys = [
{{ label: '1', value: 1, type: 'number' }}, {{ label: '2', value: 2, type: 'number' }}, {{ label: '3', value: 3, type: 'number' }},
{{ label: '4', value: 4, type: 'number' }}, {{ label: '5', value: 5, type: 'number' }}, {{ label: '6', value: 6, type: 'number' }},
{{ label: '7', value: 7, type: 'number' }}, {{ label: '8', value: 8, type: 'number' }}, {{ label: '9', value: 9, type: 'number' }},
{{ label: '*', value: '*', type: 'special' }}, {{ label: '0', value: 0, type: 'number' }}, {{ label: '#', value: '#', type: 'special' }}
];
keys.forEach(item => {{
const btn = document.createElement('button');
btn.className = 'key' + (item.type === 'special' ? ' special' : '');
btn.innerText = item.label;
btn.addEventListener('click', () => handleKeyPress(item.value));
keypad.appendChild(btn);
}});
}}
// ---------- 开始某个电话 ----------
function startPhone(phoneIdx) {{
currentPhoneIdx = phoneIdx;
currentDotIndex = 0;
isWaiting = false;
// 动态从 phones 数组中获取对应的号码和身份
const phoneData = currentStudent.phones[phoneIdx];
currentPhoneStr = phoneData.phone;
currentIdentity = phoneData.identity;
// 如果这个号码是空的(预防性处理,通常 Python 端已过滤)
if (!currentPhoneStr) {{
goToNextPhone();
return;
}}
// 🌟 修改:显示当前是第几个号码,以及一共有几个号码
const totalPhones = currentStudent.phones.length;
phoneLabel.innerText = `第 ${{phoneIdx + 1}}/${{totalPhones}} 个号码`;
identityName.innerText = currentIdentity;
identityEmoji.innerText = avatars[currentIdentity] || '👤';
hintText.innerText = '请观察有几个点,在下面按数字哦!';
hintText.style.color = '#ff6b6b';
renderDots();
updateActiveDot();
}}
// ---------- 渲染点阵 ----------
function renderDots() {{
dotsContainer.innerHTML = '';
const digits = currentPhoneStr.split('');
digits.forEach((digit, index) => {{
const group = document.createElement('div');
group.className = 'dot-group';
group.id = `dot-group-${{index}}`;
const box = document.createElement('div');
box.className = 'dot-box';
let num = parseInt(digit, 10);
if (isNaN(num)) num = 0;
if (num === 0) {{
const dot = document.createElement('div');
dot.className = 'dot zero';
box.appendChild(dot);
}} else {{
for (let i = 0; i < num; i++) {{
const dot = document.createElement('div');
dot.className = 'dot';
box.appendChild(dot);
}}
}}
const numText = document.createElement('div');
numText.className = 'dot-number';
numText.innerText = digit;
group.appendChild(box);
group.appendChild(numText);
dotsContainer.appendChild(group);
}});
}}
// ---------- 高亮当前点组 ----------
function updateActiveDot() {{
document.querySelectorAll('.dot-group').forEach(el => el.classList.remove('active'));
if (currentDotIndex < currentPhoneStr.length) {{
const currentGroup = document.getElementById(`dot-group-${{currentDotIndex}}`);
if (currentGroup) {{
currentGroup.classList.add('active');
}}
}}
}}
// ---------- 处理按键 ----------
function handleKeyPress(value) {{
if (isWaiting) return;
if (value === '*' || value === '#') return;
const targetDigit = parseInt(currentPhoneStr[currentDotIndex], 10);
if (value === targetDigit) {{
playSound('correct');
const group = document.getElementById(`dot-group-${{currentDotIndex}}`);
if (group) {{
group.querySelector('.dot-number').classList.add('show');
}}
currentDotIndex++;
if (currentDotIndex < currentPhoneStr.length) {{
updateActiveDot();
}} else {{
isWaiting = true;
setTimeout(() => dialNumber(), 800);
}}
}} else {{
playSound('wrong');
hintText.innerText = '哎呀,不对哦!再数一数,试一次吧!';
hintText.style.color = '#ef476f';
const activeBox = document.querySelector('.dot-group.active .dot-box');
if (activeBox) {{
activeBox.classList.add('shake');
setTimeout(() => activeBox.classList.remove('shake'), 400);
}}
}}
}}
// ---------- 拨号动画 ----------
function dialNumber() {{
callModal.classList.add('show');
modalAvatar.innerText = '📞';
modalSpeech.innerText = '正在拨号...';
modalBtn.style.display = 'none';
playSound('dial');
setTimeout(() => {{
playSound('connect');
modalAvatar.innerText = avatars[currentIdentity] || '👤';
const line = speechLines[Math.floor(Math.random() * speechLines.length)]
.replace(/{{name}}/g, currentStudent.name)
.replace(/{{identity}}/g, currentIdentity);
modalSpeech.innerText = line;
speakText(line);
// 动态判断是否还有下一个号码
const isLastPhone = (currentPhoneIdx === currentStudent.phones.length - 1);
modalBtn.innerText = isLastPhone ? '好啦,完成' : '听完了,打下一个电话';
modalBtn.style.display = 'inline-block';
}}, 2000);
}}
// ---------- 关闭弹窗 ----------
function closeModal() {{
callModal.classList.remove('show');
if ('speechSynthesis' in window) {{
window.speechSynthesis.cancel();
}}
goToNextPhone();
}}
// ---------- 跳到下一个号码或结束 ----------
function goToNextPhone() {{
const nextIdx = currentPhoneIdx + 1;
// 如果还有号码,继续拨打
if (nextIdx < currentStudent.phones.length) {{
startPhone(nextIdx);
}} else {{
// 所有号码都拨完了,完成该学生
completeStudent();
}}
}}
// ---------- 完成某个学生,返回卡片矩阵并置灰 ----------
function completeStudent() {{
if (currentStudent) {{
completedIds.add(currentStudent.id);
}}
backToGrid();
}}
// ---------- 音效 ----------
function playSound(type) {{
if (!audioCtx) return;
if (audioCtx.state === 'suspended') audioCtx.resume();
const now = audioCtx.currentTime;
if (type === 'correct') {{
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.type = 'sine';
osc.frequency.setValueAtTime(880, now);
osc.frequency.exponentialRampToValueAtTime(1318.51, now + 0.1);
gain.gain.setValueAtTime(0.3, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.2);
osc.start(now);
osc.stop(now + 0.2);
}} else if (type === 'wrong') {{
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(150, now);
osc.frequency.linearRampToValueAtTime(100, now + 0.2);
gain.gain.setValueAtTime(0.3, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
osc.start(now);
osc.stop(now + 0.3);
}} else if (type === 'dial') {{
const osc1 = audioCtx.createOscillator();
const gain1 = audioCtx.createGain();
osc1.connect(gain1);
gain1.connect(audioCtx.destination);
osc1.type = 'sine';
osc1.frequency.setValueAtTime(440, now);
gain1.gain.setValueAtTime(0.1, now);
osc1.start(now);
osc1.stop(now + 0.5);
setTimeout(() => {{
const osc2 = audioCtx.createOscillator();
const gain2 = audioCtx.createGain();
osc2.connect(gain2);
gain2.connect(audioCtx.destination);
osc2.type = 'sine';
osc2.frequency.setValueAtTime(440, audioCtx.currentTime);
gain2.gain.setValueAtTime(0.1, audioCtx.currentTime);
osc2.start(audioCtx.currentTime);
osc2.stop(audioCtx.currentTime + 0.5);
}}, 1000);
}} else if (type === 'connect') {{
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.type = 'triangle';
osc.frequency.setValueAtTime(523.25, now);
gain.gain.setValueAtTime(0.2, now);
osc.start(now);
osc.stop(now + 0.1);
setTimeout(() => {{
const osc2 = audioCtx.createOscillator();
const gain2 = audioCtx.createGain();
osc2.connect(gain2);
gain2.connect(audioCtx.destination);
osc2.type = 'triangle';
osc2.frequency.setValueAtTime(659.25, audioCtx.currentTime);
gain2.gain.setValueAtTime(0.2, audioCtx.currentTime);
osc2.start(audioCtx.currentTime);
osc2.stop(audioCtx.currentTime + 0.15);
}}, 120);
}}
}}
// ---------- 语音合成 ----------
function speakText(text) {{
if ('speechSynthesis' in window) {{
window.speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = 'zh-CN';
utterance.rate = 0.9;
utterance.pitch = 1.2;
const voices = window.speechSynthesis.getVoices();
const zhVoice = voices.find(v => v.lang.includes('zh'));
if (zhVoice) utterance.voice = zhVoice;
window.speechSynthesis.speak(utterance);
}}
}}
// 启动
window.onload = initGame;
</script>
</body>
</html>'''
# 3. 保存并输出
try:
os.makedirs(os.path.dirname(save_html_path), exist_ok=True)
with open(save_html_path, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f'✅ 破译电话号码游戏已成功生成!')
print(f'📁 文件保存位置: {save_html_path}')
except Exception as e:
print(f'❌ 生成 HTML 文件失败: {e}')
if __name__ == '__main__':
# ================= 配置区 =================
path = r'D:\000Python网页html\20261026破译电话号码'
EXCEL_PATH = path + r'\家长手机号码.xlsx'
SAVE_PATH = path + r'\008破译电话号码_号码总数提示版.html'
# =========================================
generate_phone_crack_game(EXCEL_PATH, SAVE_PATH)




但是如果一个幼儿有8个手机号,它一个人就要做很久,影响其他孩子操作。
明天去测试

五、如果超过2个,就选择是否要继续。不超过两个就自动返回。
后续在做
20261028小3班电脑测试
20261028小3班希沃白板测试



























版面不正确,不像宽手机。
后来我试试其他浏览器,结果谷歌浏览器可以






