国际音标魔法实验室工具HTML5实现

国际音标魔法实验室工具HTML5实现

功能全景和实现原理

这是一个设计科学、功能完善、代码规范的音标学习工具。它将枯燥的音标符号转化为可视、可听、可对比、可实战的互动体验,尤其适合中国英语学习者纠正母语负迁移导致的发音问题。

这是一个纯前端、零依赖、开箱即用的英文文本跟读工具,所有数据均在本地处理,无需联网(但因推荐用Edge 浏览器语音优质语音需要联网------断网可能导致TTS声音降级,联网朗读效果最佳)。使用Edge 浏览器音质效果最佳:

右键 本工具→ "打开方式" → Edge 浏览器,享受最佳语音体验。

该工具围绕国际音标学习 设计了三个核心模块,覆盖"认知→纠错→实战"的完整学习路径:

|--------------------|-------------------------------------------------------------------------------|----------------------------------|
| 模块 | 功能 | 教学定位 |
| 48音标图鉴 | 展示全部 48 个音标卡片,按元音/辅音分类筛选;点击卡片播放纯音标或例词发音;底部详情面板显示嘴型要领和母语避坑提示 | 认知阶段:建立音标符号与发音的初步连接 |
| 拼音 vs 音标诊所 | 针对中国学生最易犯的4大错误(/v/ vs w、/θ/ vs s、/æ/ vs ai、/əʊ/ vs o),用对比卡片展示错误习惯与正确发音,配有例词试听 | 纠错阶段:打破母语负迁移,纠正根深蒂固的发音习惯 |
| 相似音听力对决 | 15组最小对立对(Minimal Pairs),盲听发音后从两个选项中选出听到的单词,配有试听按钮即时反馈对错 | 实战阶段:训练听觉辨音能力,区分易混音素 |

发音说明:

代码采用的 "纯音拟音映射 + Edge 神经网络 TTS + 语速减慢至 0.8" 的组合拳,在无外部资源依赖的前提下,实现了体积小、发音最清晰、响应最迅速的优秀体验!

纯音标是如何发声的?"拟音映射"技术,见代码中ipaData的pureText(拟音文本)字段。我们利用英语拼写规则,将 48 个音标隐式转换成了 能诱导 TTS 引擎发出纯音标声的"假单词"。准确度: 90%------高度可用且逼真。

示例单词发音更无问题,这是 TTS 引擎最擅长的领域。在 Microsoft Edge 的神经网络(Natural)发音人很好了,且例词是优选。

针对中国学习者的特殊处理

在"诊所"模块中,直接对比了 错误习惯(拼音) 和 正确音标,并反复播放正确例词,帮助学生打破母语负迁移。

在听力对决中,使用 最小对立对(如 sheep vs ship),通过盲听训练辨音能力,TTS 对这类区别性特征的还原度很高。

运行截图

帮助部分

源码

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>🔮 国际音标魔法实验室 · 规范稳定版</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            background: radial-gradient(circle at 50% 10%, #1a2a3a, #0f2027);
            font-family: 'Segoe UI', system-ui, sans-serif;
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            color: #e0e0e0;
            padding: 20px;
        }
        .container {
            background: rgba(255, 255, 255, 0.05);
            backdrop-filter: blur(25px);
            border-radius: 35px;
            padding: 30px;
            max-width: 1000px;
            width: 100%;
            border: 1px solid rgba(255, 255, 255, 0.15);
            box-shadow: 0 30px 70px rgba(0,0,0,0.8), inset 0 0 30px rgba(0, 180, 216, 0.1);
            position: relative;
        }
        .header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 20px;
            flex-wrap: wrap;
            gap: 10px;
        }
        h1 {
            font-size: 2.2em;
            color: #a0e9ff;
            font-weight: 500;
            letter-spacing: 1px;
            text-shadow: 0 0 15px rgba(0, 180, 216, 0.5);
        }
        .subtitle { opacity: 0.8; font-size: 0.95em; margin-top: 4px; }
        
        .help-btn {
            background: rgba(255, 255, 255, 0.1);
            border: 1px solid rgba(255, 255, 255, 0.2);
            color: #90e0ef;
            padding: 8px 18px;
            border-radius: 30px;
            cursor: pointer;
            font-size: 0.95em;
            transition: 0.3s;
        }
        .help-btn:hover { background: rgba(0, 180, 216, 0.3); border-color: #00b4d8; color: #fff; }

        /* 模式选项卡 */
        .tabs {
            display: flex;
            justify-content: center;
            gap: 12px;
            margin-bottom: 25px;
            flex-wrap: wrap;
        }
        .tab-btn {
            background: rgba(255, 255, 255, 0.08);
            border: 1px solid rgba(255, 255, 255, 0.15);
            color: #aaa;
            padding: 10px 22px;
            border-radius: 30px;
            cursor: pointer;
            font-size: 1.05em;
            transition: 0.3s;
            font-weight: 500;
        }
        .tab-btn:hover { background: rgba(0, 180, 216, 0.2); color: #fff; }
        .tab-btn.active {
            background: #0077b6;
            border-color: #00b4d8;
            color: #fff;
            box-shadow: 0 0 15px rgba(0, 119, 182, 0.6);
        }

        /* 过滤器按钮 */
        .filter-bar {
            display: flex;
            justify-content: center;
            gap: 8px;
            margin-bottom: 20px;
            flex-wrap: wrap;
        }
        .filter-btn {
            background: transparent;
            border: 1px solid rgba(255,255,255,0.15);
            color: #ccc;
            padding: 5px 14px;
            border-radius: 20px;
            font-size: 0.88em;
            cursor: pointer;
            transition: 0.2s;
        }
        .filter-btn.active {
            background: #00b4d8;
            color: #0d0d2b;
            border-color: #00b4d8;
            font-weight: 600;
        }

        /* 48音标卡片网格 */
        .ipa-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(135px, 1fr));
            gap: 14px;
            max-height: 440px;
            overflow-y: auto;
            padding: 6px;
        }
        .ipa-card {
            background: rgba(255, 255, 255, 0.06);
            border: 1px solid rgba(255, 255, 255, 0.12);
            border-radius: 20px;
            padding: 14px 10px;
            text-align: center;
            cursor: pointer;
            transition: all 0.25s ease;
            position: relative;
        }
        .ipa-card:hover, .ipa-card.active {
            background: rgba(0, 180, 216, 0.2);
            border-color: #00b4d8;
            transform: translateY(-3px);
            box-shadow: 0 8px 18px rgba(0,0,0,0.4);
        }
        .ipa-symbol {
            font-size: 1.8em;
            font-weight: 600;
            color: #ffd966;
            margin-bottom: 4px;
            font-family: 'Times New Roman', serif;
        }
        .ipa-example { font-size: 0.9em; color: #ffffff; opacity: 0.9; }
        .ipa-tag {
            font-size: 0.72em; margin-top: 6px; padding: 2px 8px; border-radius: 10px; display: inline-block;
            background: rgba(255,255,255,0.1); color: #90e0ef;
        }
        .ipa-tag.voiced { color: #ff9999; background: rgba(255,100,100,0.15); } 
        .ipa-tag.voiceless { color: #99eeff; background: rgba(100,200,255,0.15); } 

        /* 音标详情展示板 */
        .detail-panel {
            background: rgba(0, 0, 0, 0.4);
            border: 1px solid rgba(0, 180, 216, 0.4);
            border-radius: 25px;
            padding: 20px;
            margin-top: 20px;
            display: flex;
            flex-wrap: wrap;
            align-items: center;
            justify-content: space-between;
            gap: 15px;
        }
        .detail-info { text-align: left; max-width: 580px; }
        .detail-symbol { font-size: 2.2em; color: #ffd966; font-weight: bold; }
        .detail-tip { font-size: 0.95em; color: #e0e0e0; margin: 6px 0; line-height: 1.5; }
        .detail-trap { font-size: 0.9em; color: #ff7675; background: rgba(255,118,117,0.1); padding: 6px 12px; border-radius: 10px; border-left: 3px solid #ff7675; }
        
        .btn-group-speak { display: flex; gap: 10px; flex-wrap: wrap; }
        .speak-btn {
            background: #0077b6; border: 1px solid #00b4d8; color: white;
            padding: 10px 20px; border-radius: 30px; font-size: 1em; cursor: pointer; transition: 0.2s;
        }
        .speak-btn:hover { background: #0096d6; box-shadow: 0 0 15px rgba(0,180,216,0.5); }
        .speak-btn.pure { background: #2c5364; border-color: #90e0ef; }
        .speak-btn.pure:hover { background: #3a6b7a; }

        /* 对比诊所样式 */
        .clinic-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin-top: 10px; }
        .clinic-card { background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 25px; padding: 20px; text-align: left; }
        .clinic-title { font-size: 1.15em; color: #ffd966; margin-bottom: 8px; font-weight: 600; }
        .clinic-example-bar {
            background: rgba(0, 180, 216, 0.12);
            border: 1px dashed rgba(0, 180, 216, 0.4);
            padding: 8px 14px;
            border-radius: 15px;
            margin-bottom: 12px;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .vs-box { display: flex; align-items: center; justify-content: space-around; background: rgba(0,0,0,0.25); padding: 12px; border-radius: 15px; margin: 10px 0; }
        .vs-item { text-align: center; }
        .vs-item .badge { font-size: 0.75em; padding: 2px 6px; border-radius: 6px; }
        .vs-item.wrong .badge { background: #ff7675; color: white; }
        .vs-item.right .badge { background: #55efc4; color: #00b894; font-weight: bold; }
        .vs-item .sound { font-size: 1.4em; margin-top: 4px; font-weight: bold; }

        /* 听力对决 */
        .quiz-box { background: rgba(0,0,0,0.3); border-radius: 25px; padding: 25px; margin-top: 10px; text-align: center; }
        .quiz-target-info { background: rgba(0, 180, 216, 0.15); border: 1px solid #00b4d8; padding: 10px 18px; border-radius: 20px; display: inline-block; color: #90e0ef; font-size: 1.05em; margin-bottom: 15px; }
        .quiz-options { display: flex; justify-content: center; gap: 20px; margin-top: 20px; flex-wrap: wrap; }
        .quiz-card-option {
            background: rgba(255,255,255,0.08); border: 2px solid rgba(255,255,255,0.2);
            color: white; padding: 15px 30px; border-radius: 25px; cursor: pointer; transition: 0.2s; min-width: 150px;
            display: flex; flex-direction: column; align-items: center;
        }
        .quiz-card-option:hover { background: rgba(0, 180, 216, 0.25); border-color: #00b4d8; }
        .listen-sub-btn { background: rgba(255,255,255,0.15); border: none; color: #ffd966; padding: 5px 12px; border-radius: 12px; font-size: 0.82em; margin-top: 8px; cursor: pointer; transition: 0.2s; }
        .listen-sub-btn:hover { background: rgba(255,255,255,0.3); }

        .ipa-grid::-webkit-scrollbar, .modal-box::-webkit-scrollbar { width: 6px; }
        .ipa-grid::-webkit-scrollbar-thumb, .modal-box::-webkit-scrollbar-thumb { background: #0077b6; border-radius: 10px; }

        /* 帮助弹窗 */
        .modal-overlay {
            display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
            background: rgba(0,0,0,0.75); backdrop-filter: blur(8px);
            justify-content: center; align-items: center; z-index: 1000; padding: 20px;
        }
        .modal-overlay.show { display: flex; }
        .modal-box {
            background: linear-gradient(135deg, #1a2a32, #0f2027);
            border: 1px solid rgba(255,255,255,0.2); border-radius: 30px; padding: 35px;
            max-width: 600px; width: 100%; color: #d4e0e6; max-height: 85vh; overflow-y: auto; text-align: left;
        }
        .modal-box h2 { color: #a0e9ff; margin-bottom: 15px; text-align: center; }
        .modal-box h3 { color: #90e0ef; margin: 14px 0 6px; }
        .modal-box p, .modal-box li { font-size: 0.95em; line-height: 1.6; opacity: 0.9; }
        .close-modal-btn { background: #0077b6; border: none; color: white; padding: 10px 20px; border-radius: 20px; cursor: pointer; width: 100%; margin-top: 20px; font-weight: bold; }
    </style>
</head>
<body>

<!-- ===== 帮助弹窗 ===== -->
<div class="modal-overlay" id="helpModal">
    <div class="modal-box">
        <h2>📖 音标实验室 · 使用指南</h2>
        <h3>🔤 整体介绍</h3>
        <p>本工具包含英语国际音标全部 <strong>48 个音素</strong>,分为三大模块:<br>
        <strong>音标图鉴</strong>(认知发音)、<strong>避坑诊所</strong>(对比纠错)、<strong>听力对决</strong>(辨音实战)。<br>
        建议按顺序学习,效果最佳。</p>

        <h3>📘 模块一:48音标图鉴与发音</h3>
        <ul>
            <li><strong>筛选分类</strong>:点击顶部按钮可按"单元音 / 双元音 / 浊辅音 / 清辅音"过滤卡片。</li>
            <li><strong>查看详情</strong>:点击任意音标卡片,底部会显示<strong>嘴型要领</strong>和<strong>母语避坑提示</strong>,这是正确发音的关键。</li>
            <li><strong>两种发音按钮</strong>:<br>
                🔹 <em>读纯音标</em> -- 朗读音标符号的近似音(仅供参考,不够精确)。<br>
                🔹 <em>读例词</em> -- 朗读包含该音标的真实单词(发音准确,<strong>推荐使用</strong>)。
            </li>
            <li><strong>学习建议</strong>:先听例词,再模仿嘴型要领,对比自己的发音。</li>
        </ul>

        <h3>⚠️ 模块二:拼音 vs 音标避坑诊所</h3>
        <ul>
            <li>专门针对中国学生<strong>最容易犯的 4 大发音错误</strong>设计:<br>
                /v/ 读成 w、/θ/ 读成 s、/æ/ 读成 ai、/əʊ/ 读成 o。</li>
            <li>每张卡片<strong>直接对比错误发音习惯与正确国际音标</strong>,并配有验证例词(如 voice、think、cat、go)。</li>
            <li>点击卡片内的 <strong>🔊 点击听发音</strong> 即可听到标准发音,反复对比纠正。</li>
        </ul>

        <h3>🎧 模块三:相似音听力辨析对决</h3>
        <ul>
            <li>从 <strong>15 组经典最小对立对</strong>(Minimal Pairs)中随机出题,如 <em>sheep vs ship</em>、<em>think vs sink</em>。</li>
            <li><strong>操作步骤</strong>:<br>
                ① 点击 <strong>📢 点击盲听播放目标发音</strong> 听系统朗读一个单词。<br>
                ② 从下方 A、B 两个选项中,<strong>选择你认为听到的那个单词</strong>。<br>
                ③ 可先点 <strong>👂 提前试听</strong> 分别听两个选项的发音,再作答。</li>
            <li>答对自动进入下一题;答错可重听再选,<strong>系统保证不连续出同一题</strong>。</li>
            <li>建议练习时先盲听,实在听不出再使用试听功能。</li>
        </ul>
        <h3>🔊 推荐发音环境</h3>
        <p>推荐使用 <strong>Microsoft Edge 浏览器</strong> 访问,系统自动调用 Azure 神经网络发音库(Natural)。</p>
        <button class="close-modal-btn" onclick="toggleHelp()">知道了,开始学习!</button>
    </div>
</div>

<!-- ===== 主容器 ===== -->
<div class="container">
    <div class="header">
        <div>
            <h1>🔮 国际音标魔法实验室</h1>
            <div class="subtitle">48音标纯发音 · 嘴型要领剖析 · 相似音听力盲测</div>
        </div>
        <button class="help-btn" onclick="toggleHelp()">❓ 使用说明</button>
    </div>

    <!-- 模式选项卡 -->
    <div class="tabs">
        <button class="tab-btn active" onclick="switchTab('chart', this)">📘 48音标图鉴与发音</button>
        <button class="tab-btn" onclick="switchTab('clinic', this)">⚠️ 拼音 vs 音标避坑诊所</button>
        <button class="tab-btn" onclick="switchTab('quiz', this)">🎧 相似音听力辨析对决</button>
    </div>

    <!-- ===== 模块一:48音标图鉴 ===== -->
    <div id="chartSection">
        <div class="filter-bar">
            <button class="filter-btn active" onclick="filterIPA('all', this)">🌟 全部 (48)</button>
            <button class="filter-btn" onclick="filterIPA('vowel_single', this)">元音: 单元音 (12)</button>
            <button class="filter-btn" onclick="filterIPA('vowel_double', this)">元音: 双元音 (8)</button>
            <button class="filter-btn" onclick="filterIPA('consonant_voiced', this)">辅音: 浊辅音 (17)</button>
            <button class="filter-btn" onclick="filterIPA('consonant_voiceless', this)">辅音: 清辅音 (11)</button>
        </div>

        <div class="ipa-grid" id="ipaGrid"></div>

        <!-- 音标详情展示板 -->
        <div class="detail-panel" id="detailPanel">
            <div class="detail-info">
                <span class="detail-symbol" id="dSymbol">/iː/</span>
                <span style="opacity:0.75; margin-left:10px;" id="dWord">代表单词: see [siː]</span>
                <div class="detail-tip" id="dTip">👄 <b>嘴型要领:</b> 嘴角向两侧尽量拉开,像开心地保持微笑,发长音。</div>
                <div class="detail-trap" id="dTrap">⚠️ <b>母语避坑:</b> 切勿发成汉语拼音短音"i",必须向两边拉扁并延长音调。</div>
            </div>
            <div class="btn-group-speak">
                <button class="speak-btn pure" onclick="speakIPAPure()">🔊 读纯音标</button>
                <button class="speak-btn" onclick="speakWordExample()">🔊 读例词</button>
            </div>
        </div>
    </div>

    <!-- ===== 模块二:拼音 vs 音标诊所 ===== -->
    <div id="clinicSection" style="display: none;">
        <div class="clinic-grid" id="clinicGrid"></div>
    </div>

    <!-- ===== 模块三:相似音对决 ===== -->
    <div id="quizSection" style="display: none;">
        <div class="quiz-box">
            <div class="quiz-target-info" id="quizTargetTitle">🎯 辨音考场:测试准备中...</div>
            <br>
            <button class="speak-btn" onclick="playQuizSound()" style="margin:10px 0 20px; font-size:1.1em;">📢 点击盲听播放目标发音</button>
            <div class="quiz-options" id="quizOptions"></div>
            <div id="quizFeedback" style="font-size:1.3em; margin-top:20px; min-height:35px;"></div>
        </div>
    </div>
</div>

<script>
    "use strict"; // 开启严格模式,禁止非标准 JS 语法

    // ============================================================
    //  1. 全局 48 音标数据库
    // ============================================================
    const ipaData = [
        { symbol: "/iː/", pureText: "eee", word: "see", type: "vowel_single", tag: "长元音", tip: "嘴角向两侧平拉,像微笑一样发长音。", trap: "不要发成拼音'i'(短音),需要延长且嘴唇要更扁。" },
        { symbol: "/ɑː/", pureText: "ahhh", word: "car", type: "vowel_single", tag: "长元音", tip: "张大嘴巴,舌头后缩,发沉稳的'啊'长音。", trap: "不要读成汉语拼音'a',张嘴要更大更深沉。" },
        { symbol: "/ɔː/", pureText: "aww", word: "door", type: "vowel_single", tag: "长元音", tip: "双唇收圆并向前突出,舌后部抬起。", trap: "区别拼音'o',嘴唇要向外突出成小圆圈。" },
        { symbol: "/uː/", pureText: "ooo", word: "too", type: "vowel_single", tag: "长元音", tip: "双唇用力收成极小圆圈,发'乌'长音。", trap: "比汉语拼音'u'嘴唇收得更小更圆更用力。" },
        { symbol: "/ɜː/", pureText: "err", word: "bird", type: "vowel_single", tag: "长元音", tip: "嘴唇放松微开,舌身平放,发'额'长音。", trap: "切勿卷舌!美音常带r音,但英音纯发/ɜː/不卷舌。" },
        { symbol: "/ɪ/", pureText: "ih", word: "sit", type: "vowel_single", tag: "短元音", tip: "嘴唇自然放松短促发音,介于拼音'i'与'e'之间。", trap: "不要发成长音/iː/,时间极短,嘴角无需刻意微笑拉扁。" },
        { symbol: "/ɒ/", pureText: "off", word: "hot", type: "vowel_single", tag: "短元音", tip: "嘴张大,双唇稍收圆,短促发音。", trap: "汉语无此音!不要发成'噢'或'阿',是张大嘴的短圆音。" },
        { symbol: "/ʊ/", pureText: "uh", word: "book", type: "vowel_single", tag: "短元音", tip: "双唇微圆稍突出,自然放松短发音。", trap: "切勿延长!比/uː/要短促,嘴唇放松不用力。" },
        { symbol: "/ʌ/", pureText: "uh", word: "cup", type: "vowel_single", tag: "短元音", tip: "嘴张半开,舌尖离开下齿,短促发'啊'。", trap: "不要读成汉字'阿',发音来自喉咙深处且短促。" },
        { symbol: "/æ/", pureText: "a", word: "cat", type: "vowel_single", tag: "短元音", tip: "嘴巴张大(可容两指),嘴角用力向两侧拉开!", trap: "⚠️ 重度避坑:绝对不要发成汉字'爱'或拼音'ai'!" },
        { symbol: "/e/", pureText: "eh", word: "bed", type: "vowel_single", tag: "短元音", tip: "嘴张开约一指宽,嘴角稍向两侧拉。", trap: "不要读成拼音'e'(鹅),它更偏向于'哎'的短音。" },
        { symbol: "/ə/", pureText: "uh", word: "about", type: "vowel_single", tag: "短元音", tip: "中央弱读音,极度放松短促发音。", trap: "英语中最常见的弱读音,不要重读。" },
        { symbol: "/eɪ/", pureText: "ay", word: "make", type: "vowel_double", tag: "双元音", tip: "由/e/向/ɪ/滑动,双唇由半开变扁。", trap: "不要发成汉字'妹'(mei)的死音,滑动过程要明显。" },
        { symbol: "/aɪ/", pureText: "eye", word: "like", type: "vowel_double", tag: "双元音", tip: "由/ɑː/向/ɪ/滑动,张大嘴再归扁。", trap: "比拼音'ai'张嘴幅度要大得多。" },
        { symbol: "/ɔɪ/", pureText: "oy", word: "boy", type: "vowel_double", tag: "双元音", tip: "由/ɔː/向/ɪ/滑动,双唇由圆变扁。", trap: "注意开头的圆唇要到位。" },
        { symbol: "/əʊ/", pureText: "oh", word: "go", type: "vowel_double", tag: "双元音", tip: "由/ə/向/ʊ/滑动,双唇由大圆变小圆。", trap: "⚠️ 别读成拼音'o'(欧)!结尾嘴唇要收圆收小。" },
        { symbol: "/aʊ/", pureText: "ow", word: "now", type: "vowel_double", tag: "双元音", tip: "由/ɑː/向/ʊ/滑动,张大嘴收圆。", trap: "比拼音'ao'嘴张得更大。" },
        { symbol: "/ɪə/", pureText: "ear", word: "hear", type: "vowel_double", tag: "双元音", tip: "由/ɪ/向/ə/自然过渡滑动。", trap: "注意两音间的连贯性。" },
        { symbol: "/eə/", pureText: "air", word: "bear", type: "vowel_double", tag: "双元音", tip: "由/e/向/ə/自然过渡滑动。", trap: "嘴巴先开一指宽再放松。" },
        { symbol: "/ʊə/", pureText: "oor", word: "tour", type: "vowel_double", tag: "双元音", tip: "由/ʊ/向/ə/自然过渡滑动。", trap: "双唇由小圆放松展开。" },
        { symbol: "/p/", pureText: "p", word: "pen", type: "consonant_voiceless", tag: "清辅音 💨", tip: "双唇紧闭,爆破出气,声带不震动。", trap: "不要带出元音!不要读成拼音'po'。" },
        { symbol: "/t/", pureText: "t", word: "tea", type: "consonant_voiceless", tag: "清辅音 💨", tip: "舌尖抵上齿龈,爆破喷气。", trap: "不要读成拼音'te'(特)。" },
        { symbol: "/k/", pureText: "k", word: "cat", type: "consonant_voiceless", tag: "清辅音 💨", tip: "舌后部抵软腭,爆发喷气。", trap: "不要读成拼音'ke'(科)。" },
        { symbol: "/f/", pureText: "f", word: "fish", type: "consonant_voiceless", tag: "清辅音 💨", tip: "上齿轻咬下唇,吹出气流。", trap: "不要读成拼音'fo'(佛)。" },
        { symbol: "/θ/", pureText: "th", word: "think", type: "consonant_voiceless", tag: "清辅音 💨", tip: "⚠️ 舌尖伸出上下齿之间!吹气!", trap: "⚠️ 重大避坑:切勿读成拼音's'(思)!必须咬舌头!" },
        { symbol: "/s/", pureText: "s", word: "see", type: "consonant_voiceless", tag: "清辅音 💨", tip: "上下齿靠拢,舌尖靠近齿龈摩擦吹气。", trap: "舌尖在牙齿内部,不要伸出来。" },
        { symbol: "/ʃ/", pureText: "sh", word: "she", type: "consonant_voiceless", tag: "清辅音 💨", tip: "双唇微亮突出成方形,摩擦吹气。", trap: "类似汉字'师',但双唇要更向前突出成方形。" },
        { symbol: "/h/", pureText: "h", word: "hat", type: "consonant_voiceless", tag: "清辅音 💨", tip: "嘴微开,哈出气流,声带不震动。", trap: "不要读成拼音'ha'(哈),纯呵气。" },
        { symbol: "/tʃ/", pureText: "ch", word: "chin", type: "consonant_voiceless", tag: "清辅音 💨", tip: "舌尖抵上齿龈,爆破出'吃'的清音。", trap: "不要带元音,短促爆破。" },
        { symbol: "/tr/", pureText: "tr", word: "tree", type: "consonant_voiceless", tag: "清辅音 💨", tip: "舌尖卷起抵齿龈,发'戳'的清音。", trap: "双唇向前突出。" },
        { symbol: "/ts/", pureText: "ts", word: "cats", type: "consonant_voiceless", tag: "清辅音 💨", tip: "舌尖抵上齿龈,发'次'的清音。", trap: "类似汉字'次',但无元音。" },
        { symbol: "/b/", pureText: "b", word: "big", type: "consonant_voiced", tag: "浊辅音 🔊", tip: "双唇紧闭,发音时声带必须震动!", trap: "区别/p/!摸喉咙必须感受到明显震动。" },
        { symbol: "/d/", pureText: "d", word: "dog", type: "consonant_voiced", tag: "浊辅音 🔊", tip: "舌尖抵上齿龈,声带震动发音。", trap: "摸喉咙感受到震动。" },
        { symbol: "/ɡ/", pureText: "g", word: "go", type: "consonant_voiced", tag: "浊辅音 🔊", tip: "舌后部抵软腭,声带震动发音。", trap: "不要读成汉字'哥'。" },
        { symbol: "/v/", pureText: "v", word: "voice", type: "consonant_voiced", tag: "浊辅音 🔊", tip: "⚠️ 上齿轻咬下唇!声带震动!", trap: "⚠️ 绝强避坑:不要发成拼音'w'(屋)!必须咬下唇!" },
        { symbol: "/ð/", pureText: "the", word: "this", type: "consonant_voiced", tag: "浊辅音 🔊", tip: "⚠️ 舌尖伸出齿间!声带震动!", trap: "不要读成拼音'z'或'd'!必须伸出舌尖。" },
        { symbol: "/z/", pureText: "z", word: "zoo", type: "consonant_voiced", tag: "浊辅音 🔊", tip: "上下齿靠拢摩擦,声带震动(蜂鸣声)。", trap: "像小蜜蜂嗡嗡响,声带震动极强。" },
        { symbol: "/ʒ/", pureText: "zh", word: "vision", type: "consonant_voiced", tag: "浊辅音 🔊", tip: "双唇突出,摩擦兼声带震动。", trap: "类似于'日',但双唇突出且带有强震动。" },
        { symbol: "/r/", pureText: "r", word: "red", type: "consonant_voiced", tag: "浊辅音 🔊", tip: "舌尖向上卷起,双唇收圆,声带震动。", trap: "不要读成汉字'日',舌尖不接触上腭。" },
        { symbol: "/dʒ/", pureText: "j", word: "job", type: "consonant_voiced", tag: "浊辅音 🔊", tip: "舌尖抵齿龈爆破兼震动。", trap: "类似于汉字'知',但声带发音更重。" },
        { symbol: "/dr/", pureText: "dr", word: "dry", type: "consonant_voiced", tag: "浊辅音 🔊", tip: "舌尖卷起,发'捉'的浊音。", trap: "声带强烈震动。" },
        { symbol: "/dz/", pureText: "dz", word: "beds", type: "consonant_voiced", tag: "浊辅音 🔊", tip: "舌尖抵齿龈,发'姿'的浊音。", trap: "声带震动。" },
        { symbol: "/m/", pureText: "m", word: "man", type: "consonant_voiced", tag: "鼻音 🔊", tip: "双唇紧闭,气流从鼻腔出来。", trap: "发音结尾闭嘴。" },
        { symbol: "/n/", pureText: "n", word: "no", type: "consonant_voiced", tag: "鼻音 🔊", tip: "舌尖抵上齿龈,气流从鼻腔出来。", trap: "张嘴鼻音。" },
        { symbol: "/ŋ/", pureText: "ng", word: "sing", type: "consonant_voiced", tag: "鼻音 🔊", tip: "舌后部抵软腭,后鼻音。", trap: "类似于拼音后鼻音'eng'的结尾。" },
        { symbol: "/l/", pureText: "l", word: "leg", type: "consonant_voiced", tag: "似音 🔊", tip: "舌尖抵上齿龈,气流从舌两侧流出。", trap: "在词尾时(如milk)舌尖也要抵住上齿龈。" },
        { symbol: "/w/", pureText: "w", word: "wet", type: "consonant_voiced", tag: "半元音 🔊", tip: "双唇收圆突出,迅速向后滑动。", trap: "不要带出'屋'的重音。" },
        { symbol: "/j/", pureText: "y", word: "yes", type: "consonant_voiced", tag: "半元音 🔊", tip: "舌前部抬高,迅速向元音滑动。", trap: "不要读成汉字'爷'。" }
    ];

    // 2. 诊所对比数据
    const clinicData = [
        {
            title: "咬唇音陷阱: /v/ vs 拼音 w",
            word: "voice", wordIPA: "/vɔɪs/",
            wrongSound: "拼音 w (屋)", wrongTip: "双唇圆圆不接触牙齿",
            rightSound: "音标 /v/", rightTip: "上齿必须轻咬下唇并震动声带"
        },
        {
            title: "咬舌音陷阱: /θ/ vs 拼音 s",
            word: "think", wordIPA: "/θɪŋk/",
            wrongSound: "拼音 s (思)", wrongTip: "舌头躲在牙齿里面",
            rightSound: "音标 /θ/", rightTip: "舌尖必须伸出上下齿之间吹气!"
        },
        {
            title: "夸张大嘴陷阱: /æ/ vs 拼音 ai",
            word: "cat", wordIPA: "/kæt/",
            wrongSound: "拼音 ai / 汉字'爱'", wrongTip: "嘴巴张开角度很小",
            rightSound: "音标 /æ/", rightTip: "嘴巴大张(可容两指),嘴角向两边夸张平拉!"
        },
        {
            title: "收尾圆唇陷阱: /əʊ/ vs 拼音 o",
            word: "go", wordIPA: "/ɡəʊ/",
            wrongSound: "拼音 o (欧)", wrongTip: "发音结尾嘴唇形状固定不变",
            rightSound: "音标 /əʊ/", rightTip: "由大圆向小圆滑动,结尾嘴唇明显收小圆!"
        }
    ];

    // 3. 听力对决题库 (15 组经典 Minimal Pairs)
    const quizPairs = [
        { groupTitle: "🎯 考点 1:长元音 /iː/ vs 短元音 /ɪ/", wordA: "sheep", symbolA: "/ʃiːp/", wordB: "ship", symbolB: "/ʃɪp/", correct: "A" },
        { groupTitle: "🎯 考点 2:长元音 /iː/ vs 短元音 /ɪ/", wordA: "seat", symbolA: "/siːt/", wordB: "sit", symbolB: "/sɪt/", correct: "A" },
        { groupTitle: "🎯 考点 3:大张嘴 /æ/ vs 小张嘴 /e/", wordA: "pan", symbolA: "/pæn/", wordB: "pen", symbolB: "/pen/", correct: "A" },
        { groupTitle: "🎯 考点 4:大张嘴 /æ/ vs 小张嘴 /e/", wordA: "bad", symbolA: "/bæd/", wordB: "bed", symbolB: "/bed/", correct: "B" },
        { groupTitle: "🎯 考点 5:大张嘴 /æ/ vs 短促音 /ʌ/", wordA: "cap", symbolA: "/kæp/", wordB: "cup", symbolB: "/kʌp/", correct: "B" },
        { groupTitle: "🎯 考点 6:长圆唇 /uː/ vs 短圆唇 /ʊ/", wordA: "fool", symbolA: "/fuːl/", wordB: "full", symbolB: "/fʊl/", correct: "A" },
        { groupTitle: "🎯 考点 7:咬舌音 /θ/ vs 齿缝音 /s/", wordA: "think", symbolA: "/θɪŋk/", wordB: "sink", symbolB: "/sɪŋk/", correct: "A" },
        { groupTitle: "🎯 考点 8:咬舌音 /θ/ vs 齿缝音 /s/", wordA: "thank", symbolA: "/θæŋk/", wordB: "sank", symbolB: "/sæŋk/", correct: "A" },
        { groupTitle: "🎯 考点 9:咬唇音 /v/ vs 圆唇音 /w/", wordA: "voice", symbolA: "/vɔɪs/", wordB: "ways", symbolB: "/weɪz/", correct: "A" },
        { groupTitle: "🎯 考点 10:咬唇音 /v/ vs 圆唇音 /w/", wordA: "vest", symbolA: "/vest/", wordB: "west", symbolB: "/west/", correct: "B" },
        { groupTitle: "🎯 考点 11:舌侧音 /l/ vs 卷舌音 /r/", wordA: "light", symbolA: "/laɪt/", wordB: "right", symbolB: "/raɪt/", correct: "B" },
        { groupTitle: "🎯 考点 12:舌侧音 /l/ vs 卷舌音 /r/", wordA: "lock", symbolA: "/lɒk/", wordB: "rock", symbolB: "/rɒk/", correct: "A" },
        { groupTitle: "🎯 考点 13:后鼻音 /ŋ/ vs 前鼻音 /n/", wordA: "thing", symbolA: "/θɪŋ/", wordB: "thin", symbolB: "/θɪn/", correct: "A" },
        { groupTitle: "🎯 考点 14:清辅音 /p/ vs 咬唇清音 /f/", wordA: "pan", symbolA: "/pæn/", wordB: "fan", symbolB: "/fæn/", correct: "B" },
        { groupTitle: "🎯 考点 15:爆破破擦音 /tʃ/ vs 擦音 /ʃ/", wordA: "chair", symbolA: "/tʃeə/", wordB: "share", symbolB: "/ʃeə/", correct: "A" }
    ];

    // ============================================================
    //  4. 状态变量与 DOM 引用
    // ============================================================
    let activeUtterance = null;
    let selectedIPA = ipaData[0];
    let currentQuiz = null;
    let lastQuizIndex = -1; // 记录上次出题索引,防连续重复

    const ipaGrid = document.getElementById('ipaGrid');
    const dSymbol = document.getElementById('dSymbol');
    const dWord = document.getElementById('dWord');
    const dTip = document.getElementById('dTip');
    const dTrap = document.getElementById('dTrap');
    const clinicGrid = document.getElementById('clinicGrid');

    // ============================================================
    //  5. 语音引擎模块
    // ============================================================
    function getBestVoice() {
        if (!window.speechSynthesis) return null;
        const voices = window.speechSynthesis.getVoices();
        let best = voices.find(v => v.lang.toLowerCase().startsWith('en') && v.name.toLowerCase().includes('natural'));
        if (!best) best = voices.find(v => v.lang.toLowerCase().startsWith('en'));
        return best;
    }

    function speakText(text) {
        if (activeUtterance) {
            activeUtterance.onend = null;
            activeUtterance.onerror = null;
        }
        window.speechSynthesis.cancel();

        const utterance = new SpeechSynthesisUtterance(text);
        utterance.lang = 'en-US';
        utterance.rate = 0.8;

        const voice = getBestVoice();
        if (voice) utterance.voice = voice;

        if (!window.activeSpeechUtterances) window.activeSpeechUtterances = [];
        window.activeSpeechUtterances.push(utterance);
        if (window.activeSpeechUtterances.length > 30) window.activeSpeechUtterances.shift();

        activeUtterance = utterance;
        window.speechSynthesis.speak(utterance);
        if (window.speechSynthesis.paused) window.speechSynthesis.resume();
    }

    function speakIPAPure() {
        if (selectedIPA) speakText(selectedIPA.pureText);
    }

    function speakWordExample() {
        if (selectedIPA) speakText(selectedIPA.word);
    }

    // ============================================================
    //  6. 模块一渲染:48 音标网格 (优化 event 传入)
    // ============================================================
    function renderIPAGrid(filter = 'all') {
        ipaGrid.innerHTML = '';
        const filtered = ipaData.filter(item => filter === 'all' || item.type === filter);

        filtered.forEach(item => {
            const card = document.createElement('div');
            card.className = `ipa-card ${selectedIPA.symbol === item.symbol ? 'active' : ''}`;
            const isVoiced = item.tag.includes('🔊');
            const tagClass = isVoiced ? 'voiced' : (item.tag.includes('💨') ? 'voiceless' : '');
            
            card.innerHTML = `
                <div class="ipa-symbol">${item.symbol}</div>
                <div class="ipa-example">${item.word}</div>
                <div class="ipa-tag ${tagClass}">${item.tag}</div>
            `;
            card.addEventListener('click', () => {
                document.querySelectorAll('.ipa-card').forEach(c => c.classList.remove('active'));
                card.classList.add('active');
                selectedIPA = item;
                updateDetailPanel();
                speakIPAPure();
            });
            ipaGrid.appendChild(card);
        });
    }

    function updateDetailPanel() {
        if (!selectedIPA) return;
        dSymbol.textContent = selectedIPA.symbol;
        dWord.textContent = `代表单词: ${selectedIPA.word}`;
        dTip.innerHTML = `👄 <b>嘴型要领:</b> ${selectedIPA.tip}`;
        dTrap.innerHTML = `⚠️ <b>母语避坑:</b> ${selectedIPA.trap}`;
    }

    // ★ 修复点 1:显式接收 btnEl,不依赖全局 event 对象
    function filterIPA(type, btnEl) {
        document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active'));
        if (btnEl) btnEl.classList.add('active');
        renderIPAGrid(type);
    }

    // ============================================================
    //  7. 模块二渲染:对比诊所 (DOM 监听规范绑定)
    // ============================================================
    function renderClinic() {
        clinicGrid.innerHTML = '';
        clinicData.forEach(item => {
            const card = document.createElement('div');
            card.className = 'clinic-card';
            
            const titleEl = document.createElement('div');
            titleEl.className = 'clinic-title';
            titleEl.textContent = item.title;

            const exampleBar = document.createElement('div');
            exampleBar.className = 'clinic-example-bar';
            exampleBar.innerHTML = `
                <span>📢 <b>诊所验证例词:</b> <span style="color:#ffd966; font-size:1.1em; margin-left:4px;">${item.word}</span> <span style="opacity:0.75;">${item.wordIPA}</span></span>
            `;

            const speakBtn = document.createElement('button');
            speakBtn.className = 'speak-btn';
            speakBtn.style.cssText = 'padding:3px 10px; font-size:0.8em;';
            speakBtn.textContent = '🔊 点击听发音';
            // ★ 修复点 2:使用标准 addEventListener 代替字符串内联 onclick
            speakBtn.addEventListener('click', () => speakText(item.word));
            exampleBar.appendChild(speakBtn);

            const vsBox = document.createElement('div');
            vsBox.className = 'vs-box';
            vsBox.innerHTML = `
                <div class="vs-item wrong">
                    <span class="badge">❌ 错误发音习惯</span>
                    <div class="sound" style="color:#ff7675;">${item.wrongSound}</div>
                    <div style="font-size:0.75em; opacity:0.8; margin-top:4px;">${item.wrongTip}</div>
                </div>
                <div style="font-size:1.5em; font-weight:bold; opacity:0.3;">VS</div>
                <div class="vs-item right">
                    <span class="badge">✅ 正确国际发音</span>
                    <div class="sound" style="color:#55efc4;">${item.rightSound}</div>
                    <div style="font-size:0.75em; opacity:0.8; margin-top:4px;">${item.rightTip}</div>
                </div>
            `;

            card.appendChild(titleEl);
            card.appendChild(exampleBar);
            card.appendChild(vsBox);
            clinicGrid.appendChild(card);
        });
    }

    // ============================================================
    //  8. 模块三逻辑:听力盲测对决 (防连续重复 + DOM 标准事件)
    // ============================================================
    function setupQuiz() {
        // ★ 修复点 3:增加防重复抽题机制,避免连续出现相同题目
        let nextIndex;
        do {
            nextIndex = Math.floor(Math.random() * quizPairs.length);
        } while (nextIndex === lastQuizIndex && quizPairs.length > 1);
        
        lastQuizIndex = nextIndex;
        currentQuiz = quizPairs[nextIndex];

        const container = document.getElementById('quizOptions');
        const feedback = document.getElementById('quizFeedback');
        const targetTitle = document.getElementById('quizTargetTitle');

        targetTitle.textContent = `${currentQuiz.groupTitle}`;
        feedback.textContent = '';
        container.innerHTML = '';

        // 构建 选项 A
        const cardA = document.createElement('div');
        cardA.className = 'quiz-card-option';
        cardA.innerHTML = `
            <div style="font-size:1.4em; font-weight:bold;">${currentQuiz.wordA}</div>
            <div style="color:#ffd966; font-size:1.1em; margin:2px 0;">${currentQuiz.symbolA}</div>
        `;
        const btnListenA = document.createElement('button');
        btnListenA.className = 'listen-sub-btn';
        btnListenA.textContent = '👂 提前试听 A';
        // ★ 修复点 4:标准 e.stopPropagation(),杜绝隐式 event
        btnListenA.addEventListener('click', (e) => {
            e.stopPropagation();
            speakText(currentQuiz.wordA);
        });
        cardA.appendChild(btnListenA);
        cardA.addEventListener('click', () => checkQuiz('A'));

        // 构建 选项 B
        const cardB = document.createElement('div');
        cardB.className = 'quiz-card-option';
        cardB.innerHTML = `
            <div style="font-size:1.4em; font-weight:bold;">${currentQuiz.wordB}</div>
            <div style="color:#ffd966; font-size:1.1em; margin:2px 0;">${currentQuiz.symbolB}</div>
        `;
        const btnListenB = document.createElement('button');
        btnListenB.className = 'listen-sub-btn';
        btnListenB.textContent = '👂 提前试听 B';
        btnListenB.addEventListener('click', (e) => {
            e.stopPropagation();
            speakText(currentQuiz.wordB);
        });
        cardB.appendChild(btnListenB);
        cardB.addEventListener('click', () => checkQuiz('B'));

        container.appendChild(cardA);
        container.appendChild(cardB);
    }

    function playQuizSound() {
        if (!currentQuiz) return;
        const targetWord = currentQuiz.correct === 'A' ? currentQuiz.wordA : currentQuiz.wordB;
        speakText(targetWord);
    }

    function checkQuiz(choice) {
        const feedback = document.getElementById('quizFeedback');
        if (choice === currentQuiz.correct) {
            feedback.innerHTML = '🎉 <span style="color:#55efc4;">辨音敏锐!完全正确!自动进入下一题...</span>';
            setTimeout(setupQuiz, 1400);
        } else {
            feedback.innerHTML = '❌ <span style="color:#ff7675;">耳朵偷懒了,点击【📢 点击盲听】再听一次!</span>';
            playQuizSound();
        }
    }

    // ============================================================
    //  9. 模式切换与初始化 (标准句柄显式传递)
    // ============================================================
    function switchTab(mode, btnEl) {
        document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
        if (btnEl) btnEl.classList.add('active');

        document.getElementById('chartSection').style.display = mode === 'chart' ? 'block' : 'none';
        document.getElementById('clinicSection').style.display = mode === 'clinic' ? 'block' : 'none';
        document.getElementById('quizSection').style.display = mode === 'quiz' ? 'block' : 'none';

        if (mode === 'quiz') setupQuiz();
    }

    function toggleHelp() {
        document.getElementById('helpModal').classList.toggle('show');
    }

    window.onload = function() {
        renderIPAGrid();
        updateDetailPanel();
        renderClinic();
        if (window.speechSynthesis) {
            window.speechSynthesis.onvoiceschanged = () => getBestVoice();
        }
    };
</script>
</body>
</html>
相关推荐
FogLetter2 小时前
嘘!WebSocket正在“偷听”你的网络请求——全双工通信的魔法
前端·面试
用户938515635072 小时前
从零构建 React Todo 应用:组件化设计与数据流深度解析
前端·javascript
IT_陈寒2 小时前
Python线程池把我坑惨了,这些盲区你不踩?
前端·人工智能·后端
程序员爱钓鱼2 小时前
第一个 Go 程序:Hello World
前端·后端·go
阳光是sunny2 小时前
LangGraph 核心概念详解:从编译到可视化
前端·人工智能·后端
不好听6133 小时前
React 父子组件通信:从 Todo 应用理解单向数据流
前端·react.js
_lucas3 小时前
做了一个glsl在线调试工具
前端·javascript·three.js
不好听61311 小时前
从一行 JSX 到屏幕像素:前端开发者必须懂的浏览器渲染管线
前端
恒拓高科WorkPlus12 小时前
企业级内网即时通讯建设模型:BeeWorks内网IM四层架构
前端