国际音标魔法实验室工具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>