【教学类-07-12】20261028大3班破译电话号码html(手机按钮版本)

一、背景需求

前期制作真实手机号的《破译电话号码(点数对应)》每位幼儿两个手机号码。

现在我想把纸质版变成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班希沃白板测试

版面不正确,不像宽手机。

后来我试试其他浏览器,结果谷歌浏览器可以

相关推荐
摇滚侠1 小时前
《On Java 中文版 基础卷》阅读笔记 复用 13
java·笔记·python
用户298698530141 小时前
Python 将 Word 文档转换为 XML 的实践要点
后端·python·api
薛之谦_1 小时前
用 Java 实现阿里云式 IAM 子用户管理:架构设计与表结构详解
java·python·阿里云
2601_952047791 小时前
金蝶云星空销售订单附件查询接口权威教程(BOS_Attachment)
python·机器人·钉钉
神仙别闹1 小时前
基于Python Yolo实现绝缘子目标检测程序
python·yolo·目标检测
超级架构师1 小时前
让 COBOL 长出 AI 大脑:原生 Agent 核心框架 praxo-core 深度剖析
c语言·开发语言·c++·人工智能·python·架构·ai编程
甜到心里的蛋糕1 小时前
抖音评论自动回复机器人部署实践:无头 Chrome + DeepSeek + NapCat(2C2G 云主机)
java·jvm·chrome·python·抖音·deepseek
砚凝霜1 小时前
CTF Misc 签到 GXUST·CTF:查看源码 + HTML 注释 + Base64 解码(附 Base64 原理)
前端·html
Thanks_ks2 小时前
从 0.1+0.2 不等于 0.3 说起:聊聊浮点数精度问题
java·python·数值计算·精度丢失·bigdecimal·浮点数·ieee754