基于JavaScript+HTML實(shí)現(xiàn)文章逐句高亮朗讀功能
效果演示


項(xiàng)目核心
本項(xiàng)目主要包含以下核心功能:
- 語音合成(Text-to-Speech)功能
- 控制播放、暫停、繼續(xù)和停止操作
- 語音選擇功能
- 閱讀進(jìn)度保存與恢復(fù)
- 句子級(jí)高亮顯示
- 點(diǎn)擊任意句子直接跳轉(zhuǎn)并朗讀
頁面結(jié)構(gòu)
控制區(qū)域
包含所有操作按鈕(開始、暫停、繼續(xù)、停止、重置)和語音選擇下拉框。
<div class="controls">
<button id="playBtn">開始朗讀</button>
<button id="pauseBtn" disabled>暫停</button>
<button id="resumeBtn" disabled>繼續(xù)</button>
<button id="stopBtn" disabled>停止</button>
<select id="voiceSelect" class="voice-select"></select>
<button id="resetBtn">重置進(jìn)度</button>
</div>
文章區(qū)域
包含多個(gè)段落,每個(gè)段落由多個(gè)可交互的句子組成。
<div class="article" id="article">
<p class="paragraph">
<span class="sentence">在編程的世界里,學(xué)習(xí)是一個(gè)永無止境的過程。</span>
<span class="sentence">隨著技術(shù)的不斷發(fā)展,我們需要不斷更新自己的知識(shí)和技能。</span>
<span class="sentence">HTML、CSS和JavaScript是構(gòu)建現(xiàn)代網(wǎng)頁的三大基石。</span>
</p>
<p class="paragraph">
<span class="sentence">掌握這些基礎(chǔ)技術(shù)后,你可以進(jìn)一步學(xué)習(xí)各種前端框架和工具。</span>
<span class="sentence">React、Vue和Angular是目前最流行的前端框架。</span>
<span class="sentence">它們都采用了組件化的開發(fā)模式,提高了代碼的可維護(hù)性和復(fù)用性。</span>
</p>
<p class="paragraph">
<span class="sentence">除了前端技術(shù),后端開發(fā)也是全棧工程師必須掌握的技能。</span>
<span class="sentence">Node.js讓JavaScript可以用于服務(wù)器端編程,大大擴(kuò)展了JavaScript的應(yīng)用范圍。</span>
<span class="sentence">數(shù)據(jù)庫技術(shù)也是開發(fā)中的重要組成部分。</span>
</p>
</div>
進(jìn)度信息
顯示當(dāng)前閱讀進(jìn)度。
<div class="progress-info">
當(dāng)前進(jìn)度: <span id="progressText">0/0</span>
<div class="progress-bar-container">
<div class="progress-bar"></div>
</div>
</div>
核心功能實(shí)現(xiàn)
定義基礎(chǔ)變量
獲取DOM元素
const sentences = document.querySelectorAll('.sentence');
const playBtn = document.getElementById('playBtn');
const pauseBtn = document.getElementById('pauseBtn');
const resumeBtn = document.getElementById('resumeBtn');
const stopBtn = document.getElementById('stopBtn');
const resetBtn = document.getElementById('resetBtn');
const voiceSelect = document.getElementById('voiceSelect');
const progressText = document.getElementById('progressText');
const progressBar = document.querySelector('.progress-bar');
定義語音合成相關(guān)變量
let speechSynthesis = window.speechSynthesis; let voices = []; let currentUtterance = null; let currentSentenceIndex = 0; let isPaused = false;
語音合成初始化
通過 window.speechSynthesis API 獲取系統(tǒng)支持的語音列表,并填充到下拉選擇框中。
function initSpeechSynthesis() {
// 獲取可用的語音列表
voices = speechSynthesis.getVoices();
// 填充語音選擇下拉框
voiceSelect.innerHTML = '';
voices.forEach((voice, index) => {
const option = document.createElement('option');
option.value = index;
option.textContent = `${voice.name} (${voice.lang})`;
voiceSelect.appendChild(option);
});
// 嘗試選擇中文語音
const chineseVoice = voices.find(voice =>{
voice.lang.includes('zh') || voice.lang.includes('cmn')
});
if (chineseVoice) {
const voiceIndex = voices.indexOf(chineseVoice);
voiceSelect.value = voiceIndex;
}
}
句子朗讀功能
function speakSentence(index) {
if (index >= sentences.length || index < 0) return;
// 停止當(dāng)前朗讀
if (currentUtterance) {
speechSynthesis.cancel();
}
// 更新當(dāng)前句子高亮
updateHighlight(index);
// 創(chuàng)建新的語音合成實(shí)例
const selectedVoiceIndex = voiceSelect.value;
const utterance = new SpeechSynthesisUtterance(sentences[index].textContent);
if (voices[selectedVoiceIndex]) {
utterance.voice = voices[selectedVoiceIndex];
}
utterance.rate = 0.9; // 稍微慢一點(diǎn)的語速
// 朗讀開始時(shí)的處理
utterance.onstart = function() {
sentences[index].classList.add('reading');
playBtn.disabled = true;
pauseBtn.disabled = false;
resumeBtn.disabled = true;
stopBtn.disabled = false;
};
// 朗讀結(jié)束時(shí)的處理
utterance.onend = function() {
sentences[index].classList.remove('reading');
if (!isPaused) {
if (currentSentenceIndex >= sentences.length - 1) {
// 朗讀完成
playBtn.disabled = false;
pauseBtn.disabled = true;
resumeBtn.disabled = true;
stopBtn.disabled = true;
updateProgressText();
return;
}
currentSentenceIndex++;
saveProgress();
speakSentence(currentSentenceIndex);
}
};
// 開始朗讀
currentUtterance = utterance;
speechSynthesis.speak(utterance);
updateProgressText();
}
句子高亮功能
function updateHighlight(index) {
sentences.forEach((sentence, i) => {
sentence.classList.remove('current');
if (i === index) {
sentence.classList.add('current');
// 滾動(dòng)到當(dāng)前句子
sentence.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
});
}
更新進(jìn)度文本
function updateProgressText() {
progressText.textContent = `${currentSentenceIndex + 1}/${sentences.length}`;
const percentage = (currentSentenceIndex + 1) / sentences.length * 100;
progressBar.style.width = `${percentage}%`;
}
進(jìn)度保存與恢復(fù)
保存進(jìn)度到本地存儲(chǔ)
function saveProgress() {
localStorage.setItem('readingProgress', currentSentenceIndex);
localStorage.setItem('articleId', 'demoArticle');
updateProgressText();
}
從本地存儲(chǔ)加載進(jìn)度
function loadProgress() {
const savedArticleId = localStorage.getItem('articleId');
if (savedArticleId === 'demoArticle') {
const savedProgress = localStorage.getItem('readingProgress');
if (savedProgress !== null) {
currentSentenceIndex = parseInt(savedProgress);
if (currentSentenceIndex >= sentences.length) {
currentSentenceIndex = 0;
}
updateHighlight(currentSentenceIndex);
updateProgressText();
}
}
}
點(diǎn)擊句子朗讀跳轉(zhuǎn)功能
sentences.forEach((sentence, index) => {
sentence.addEventListener('click', function() {
currentSentenceIndex = index;
speakSentence(currentSentenceIndex);
});
});
擴(kuò)展建議
- 語速調(diào)節(jié):增加語速調(diào)節(jié)滑塊,讓用戶自定義朗讀速
- 多語言支持:自動(dòng)檢測文本語言并選擇合適的語音引擎
- 斷句優(yōu)化:改進(jìn)自然語言處理邏輯,使朗讀更符合口語習(xí)慣
- 多文章支持:擴(kuò)展文章管理系統(tǒng),允許用戶選擇不同文章進(jìn)行朗讀
完整代碼
<!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>
body {
font-family: 'Microsoft YaHei', sans-serif;
line-height: 1.6;
max-width: 800px;
margin: 0 auto;
padding: 40px 20px;
color: #333;
height: 100vh;
box-sizing: border-box;
background: linear-gradient(to bottom right, #f8f9fa, #e9ecef);
}
h1 {
text-align: center;
color: #2c3e50;
margin-bottom: 40px;
font-size: 2.5em;
letter-spacing: 2px;
position: relative;
animation: fadeInDown 1s ease-out forwards;
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
h1::after {
content: '';
display: block;
width: 100px;
height: 4px;
background: linear-gradient(to right, #3498db, #2980b9);
margin: 15px auto 0;
border-radius: 2px;
animation: growLine 1s ease-out forwards;
}
@keyframes growLine {
from {
width: 0;
}
to {
width: 100px;
}
}
.controls {
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
border-radius: 10px;
padding: 20px;
background-color: #ffffffcc;
display: flex;
flex-direction: column;
gap: 15px;
margin-bottom: 30px;
}
.controls > div {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 15px;
}
button {
padding: 10px 20px;
background: linear-gradient(135deg, #3498db, #2980b9);
color: white;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s ease-in-out;
box-shadow: 0 4px 6px rgba(52, 152, 219, 0.3);
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(52, 152, 219, 0.4);
}
button:disabled {
background: linear-gradient(135deg, #95a5a6, #7f8c8d);
box-shadow: none;
transform: none;
}
.article {
font-size: 18px;
line-height: 1.8;
background-color: #ffffffee;
border-radius: 10px;
padding: 25px;
margin-top: 30px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.05);
margin-bottom: 30px;
position: relative;
z-index: 0
}
.article::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle at top left, rgba(52, 152, 219, 0.05) 0%, transparent 100%);
z-index: -1;
border-radius: 10px;
}
.paragraph {
margin-bottom: 20px;
}
.sentence {
border-radius: 3px;
transition: all 0.3s ease-in-out;
cursor: pointer;
position: relative;
z-index: 1;
}
.sentence:hover {
background-color: #f0f0f0;
}
.sentence::after {
content: '';
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.3);
opacity: 0;
z-index: -1;
transition: opacity 0.3s ease-in-out;
}
.sentence:hover::after {
opacity: 1;
}
.current {
background-color: #fffde7 !important;
font-weight: bold;
transform: scale(1.05);
box-shadow: 0 2px 8px rgba(255, 221, 0, 0.3);
}
.progress-info {
text-align: center;
margin-top: 20px;
font-size: 14px;
color: #7f8c8d;
}
select {
padding: 8px;
border-radius: 4px;
border: 1px solid #bdc3c7;
font-size: 16px;
}
.voice-select {
min-width: 220px;
padding: 10px 12px;
border-radius: 25px;
border: 1px solid #bdc3c7;
font-size: 16px;
background-color: #f8f9fa;
transition: all 0.3s ease-in-out;
appearance: none;
background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24'%3E%3Cpath fill='%23555' d='M7 10l5 5 5-5z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 15px center;
background-size: 12px;
display: block;
margin: 0 auto;
}
.voice-select:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
}
.progress-info {
text-align: center;
margin-top: 30px;
font-size: 14px;
color: #7f8c8d;
position: relative;
height: 30px;
}
.progress-bar-container {
width: 100%;
height: 6px;
background-color: #ecf0f1;
border-radius: 3px;
overflow: hidden;
margin: 10px 0;
}
.progress-bar {
height: 100%;
width: 0;
background: linear-gradient(to right, #3498db, #2980b9);
transition: width 0.3s ease-in-out;
}
</style>
</head>
<body>
<h1>文章逐句高亮朗讀</h1>
<div class="controls">
<div>
<button id="playBtn">開始朗讀</button>
<button id="pauseBtn" disabled>暫停</button>
<button id="resumeBtn" disabled>繼續(xù)</button>
<button id="stopBtn" disabled>停止</button>
<button id="resetBtn">重置進(jìn)度</button>
</div>
<select id="voiceSelect" class="voice-select"></select>
</div>
<div class="article" id="article">
<p class="paragraph">
<span class="sentence">在編程的世界里,學(xué)習(xí)是一個(gè)永無止境的過程。</span>
<span class="sentence">隨著技術(shù)的不斷發(fā)展,我們需要不斷更新自己的知識(shí)和技能。</span>
<span class="sentence">HTML、CSS和JavaScript是構(gòu)建現(xiàn)代網(wǎng)頁的三大基石。</span>
</p>
<p class="paragraph">
<span class="sentence">掌握這些基礎(chǔ)技術(shù)后,你可以進(jìn)一步學(xué)習(xí)各種前端框架和工具。</span>
<span class="sentence">React、Vue和Angular是目前最流行的前端框架。</span>
<span class="sentence">它們都采用了組件化的開發(fā)模式,提高了代碼的可維護(hù)性和復(fù)用性。</span>
</p>
<p class="paragraph">
<span class="sentence">除了前端技術(shù),后端開發(fā)也是全棧工程師必須掌握的技能。</span>
<span class="sentence">Node.js讓JavaScript可以用于服務(wù)器端編程,大大擴(kuò)展了JavaScript的應(yīng)用范圍。</span>
<span class="sentence">數(shù)據(jù)庫技術(shù)也是開發(fā)中的重要組成部分。</span>
</p>
</div>
<div class="progress-info">
當(dāng)前進(jìn)度: <span id="progressText">0/0</span>
<div class="progress-bar-container">
<div class="progress-bar"></div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 獲取DOM元素
const sentences = document.querySelectorAll('.sentence');
const playBtn = document.getElementById('playBtn');
const pauseBtn = document.getElementById('pauseBtn');
const resumeBtn = document.getElementById('resumeBtn');
const stopBtn = document.getElementById('stopBtn');
const resetBtn = document.getElementById('resetBtn');
const voiceSelect = document.getElementById('voiceSelect');
const progressText = document.getElementById('progressText');
const progressBar = document.querySelector('.progress-bar');
// 語音合成相關(guān)變量
let speechSynthesis = window.speechSynthesis;
let voices = [];
let currentUtterance = null;
let currentSentenceIndex = 0;
let isPaused = false;
// 從本地存儲(chǔ)加載進(jìn)度
loadProgress();
// 初始化語音合成
function initSpeechSynthesis() {
// 獲取可用的語音列表
voices = speechSynthesis.getVoices();
// 填充語音選擇下拉框
voiceSelect.innerHTML = '';
voices.forEach((voice, index) => {
const option = document.createElement('option');
option.value = index;
option.textContent = `${voice.name} (${voice.lang})`;
voiceSelect.appendChild(option);
});
// 嘗試選擇中文語音
const chineseVoice = voices.find(voice =>{
voice.lang.includes('zh') || voice.lang.includes('cmn')
});
if (chineseVoice) {
const voiceIndex = voices.indexOf(chineseVoice);
voiceSelect.value = voiceIndex;
}
}
// 語音列表加載可能需要時(shí)間
speechSynthesis.onvoiceschanged = initSpeechSynthesis;
initSpeechSynthesis();
// 朗讀指定句子
function speakSentence(index) {
if (index >= sentences.length || index < 0) return;
// 停止當(dāng)前朗讀
if (currentUtterance) {
speechSynthesis.cancel();
}
// 更新當(dāng)前句子高亮
updateHighlight(index);
// 創(chuàng)建新的語音合成實(shí)例
const selectedVoiceIndex = voiceSelect.value;
const utterance = new SpeechSynthesisUtterance(sentences[index].textContent);
if (voices[selectedVoiceIndex]) {
utterance.voice = voices[selectedVoiceIndex];
}
utterance.rate = 0.9; // 稍微慢一點(diǎn)的語速
// 朗讀開始時(shí)的處理
utterance.onstart = function() {
sentences[index].classList.add('reading');
playBtn.disabled = true;
pauseBtn.disabled = false;
resumeBtn.disabled = true;
stopBtn.disabled = false;
};
// 朗讀結(jié)束時(shí)的處理
utterance.onend = function() {
sentences[index].classList.remove('reading');
if (!isPaused) {
if (currentSentenceIndex >= sentences.length - 1) {
// 朗讀完成
playBtn.disabled = false;
pauseBtn.disabled = true;
resumeBtn.disabled = true;
stopBtn.disabled = true;
updateProgressText();
return;
}
currentSentenceIndex++;
saveProgress();
speakSentence(currentSentenceIndex);
}
};
// 開始朗讀
currentUtterance = utterance;
speechSynthesis.speak(utterance);
updateProgressText();
}
// 更新句子高亮
function updateHighlight(index) {
sentences.forEach((sentence, i) => {
sentence.classList.remove('current');
if (i === index) {
sentence.classList.add('current');
// 滾動(dòng)到當(dāng)前句子
sentence.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
});
}
// 更新進(jìn)度文本
function updateProgressText() {
progressText.textContent = `${currentSentenceIndex + 1}/${sentences.length}`;
const percentage = (currentSentenceIndex + 1) / sentences.length * 100;
progressBar.style.width = `${percentage}%`;
}
// 保存進(jìn)度到本地存儲(chǔ)
function saveProgress() {
localStorage.setItem('readingProgress', currentSentenceIndex);
localStorage.setItem('articleId', 'demoArticle'); // 在實(shí)際應(yīng)用中可以使用文章ID
updateProgressText();
}
// 從本地存儲(chǔ)加載進(jìn)度
function loadProgress() {
const savedArticleId = localStorage.getItem('articleId');
if (savedArticleId === 'demoArticle') {
const savedProgress = localStorage.getItem('readingProgress');
if (savedProgress !== null) {
currentSentenceIndex = parseInt(savedProgress);
if (currentSentenceIndex >= sentences.length) {
currentSentenceIndex = 0;
}
updateHighlight(currentSentenceIndex);
updateProgressText();
}
}
}
// 事件監(jiān)聽器
playBtn.addEventListener('click', function() {
currentSentenceIndex = 0;
speakSentence(currentSentenceIndex);
});
pauseBtn.addEventListener('click', function() {
if (speechSynthesis.speaking && !isPaused) {
speechSynthesis.pause();
isPaused = true;
pauseBtn.disabled = true;
resumeBtn.disabled = false;
}
});
resumeBtn.addEventListener('click', function() {
if (isPaused) {
speechSynthesis.resume();
isPaused = false;
pauseBtn.disabled = false;
resumeBtn.disabled = true;
}
});
stopBtn.addEventListener('click', function() {
speechSynthesis.cancel();
isPaused = false;
playBtn.disabled = false;
pauseBtn.disabled = true;
resumeBtn.disabled = true;
stopBtn.disabled = true;
// 移除所有朗讀樣式
sentences.forEach(sentence => {
sentence.classList.remove('reading');
});
});
resetBtn.addEventListener('click', function() {
localStorage.removeItem('readingProgress');
localStorage.removeItem('articleId');
currentSentenceIndex = 0;
updateHighlight(currentSentenceIndex);
updateProgressText();
});
// 點(diǎn)擊句子跳轉(zhuǎn)到該句子并朗讀
sentences.forEach((sentence, index) => {
sentence.addEventListener('click', function() {
currentSentenceIndex = index;
speakSentence(currentSentenceIndex);
});
});
});
</script>
</body>
</html>
到此這篇關(guān)于基于JavaScript+HTML實(shí)現(xiàn)文章逐句高亮朗讀功能的文章就介紹到這了,更多相關(guān)JavaScript HTML文章高亮朗讀內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
js改變img標(biāo)簽的src屬性在IE下沒反應(yīng)的解決方法
在Chrome FF里都能改變成功,但在IE下卻不行,網(wǎng)上搜了半天,大概了解了,這個(gè)是IE的一個(gè)bug,具體的解決方法如下,有類似問題的朋友可以參考下哈,希望對(duì)大家有所幫助2013-07-07
layui-tree實(shí)現(xiàn)Ajax異步請(qǐng)求后動(dòng)態(tài)添加節(jié)點(diǎn)的方法
今天小編就為大家分享一篇layui-tree實(shí)現(xiàn)Ajax異步請(qǐng)求后動(dòng)態(tài)添加節(jié)點(diǎn)的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-09-09
使用canvas實(shí)現(xiàn)鯉魚躍龍門的動(dòng)畫效果
這篇文章主要給大家介紹了使用canvas實(shí)現(xiàn)鯉魚躍龍門的動(dòng)畫效果,文中通過代碼示例給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,感興趣的小伙伴可以自己動(dòng)手嘗試一下2024-02-02
小程序?qū)崿F(xiàn)列表多個(gè)批量倒計(jì)時(shí)
這篇文章主要為大家詳細(xì)介紹了小程序?qū)崿F(xiàn)列表多個(gè)批量倒計(jì)時(shí),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-02-02
Js遍歷鍵值對(duì)形式對(duì)象或Map形式的方法
下面小編就為大家?guī)硪黄狫s遍歷鍵值對(duì)形式對(duì)象或Map形式的方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-08-08
IE 下Enter提交表單存在重復(fù)提交問題的解決方法
這篇文章主要介紹了IE 下Enter提交表單存在重復(fù)提交問題的解決方法,需要的朋友可以參考下2014-05-05
JS實(shí)現(xiàn)點(diǎn)擊Radio動(dòng)態(tài)更新table數(shù)據(jù)
這篇文章主要介紹了JS實(shí)現(xiàn)點(diǎn)擊Radio動(dòng)態(tài)更新table數(shù)據(jù)的相關(guān)資料,需要的朋友可以參考下2017-07-07

