Files
RGKT/rg-09112127/html/play/静夜思古诗.html
2025-10-10 19:35:04 +08:00

1044 lines
24 KiB
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>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- 引入后端集成脚本 -->
<script src="../../js/apiService.js"></script>
<script src="../../js/dataManager.js"></script>
<script src="../../js/userManager.js"></script>
<script src="../../js/accessTracker.js"></script>
<script src="../../js/gameTracker.js"></script>
<script src="../../js/gameDataLogger.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Comic Sans MS', 'Marker Felt', '幼圆', sans-serif;
}
body {
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
min-height: 100vh;
padding: 20px;
overflow-x: hidden;
position: relative;
}
/* 装饰元素 */
.cloud {
position: absolute;
background: white;
border-radius: 50%;
opacity: 0.7;
z-index: -1;
animation: float 15s infinite linear;
}
.cloud:nth-child(1) {
width: 120px;
height: 40px;
top: 10%;
left: 5%;
animation-duration: 20s;
}
.cloud:nth-child(2) {
width: 180px;
height: 60px;
top: 25%;
right: 8%;
animation-duration: 25s;
}
.cloud:nth-child(3) {
width: 100px;
height: 35px;
bottom: 15%;
left: 15%;
animation-duration: 18s;
}
.star {
position: absolute;
background: #ffeb3b;
clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
opacity: 0.8;
z-index: -1;
animation: twinkle 3s infinite alternate;
}
.star:nth-child(4) {
width: 25px;
height: 25px;
top: 15%;
right: 20%;
animation-delay: 0.5s;
}
.star:nth-child(5) {
width: 20px;
height: 20px;
bottom: 20%;
left: 25%;
animation-delay: 1s;
}
@keyframes float {
0% {
transform: translateX(0);
}
50% {
transform: translateX(20px);
}
100% {
transform: translateX(0);
}
}
@keyframes twinkle {
0% {
opacity: 0.3;
transform: scale(0.9);
}
100% {
opacity: 1;
transform: scale(1.1);
}
}
.container {
max-width: 900px;
margin: 0 auto;
background-color: rgba(255, 255, 255, 0.9);
border-radius: 25px;
padding: 30px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
position: relative;
overflow: hidden;
border: 8px solid #ffd6e7;
}
.container::before {
content: "";
position: absolute;
top: -10px;
left: -10px;
right: -10px;
bottom: -10px;
border: 4px dashed #ff9ec0;
border-radius: 30px;
z-index: -1;
}
header {
text-align: center;
margin-bottom: 30px;
position: relative;
}
h1 {
color: #ff6b9c;
font-size: 2.8rem;
margin-bottom: 15px;
text-shadow: 3px 3px 0 #ffe0eb;
position: relative;
display: inline-block;
}
h1::after {
content: "✏️";
position: absolute;
right: -40px;
top: -10px;
font-size: 2rem;
}
.subtitle {
color: #5a7bd3;
font-size: 1.3rem;
margin-top: -10px;
font-weight: bold;
}
.content-area {
display: flex;
flex-wrap: wrap;
gap: 30px;
margin-bottom: 30px;
}
.poem-section {
flex: 1;
min-width: 300px;
background: linear-gradient(to bottom right, #e0f7ff, #ffebf3);
border-radius: 20px;
padding: 25px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
border: 4px solid #b8e8ff;
position: relative;
}
.poem-section h2,
.recognition-section h2 {
color: #ff6b9c;
text-align: center;
margin-bottom: 20px;
font-size: 1.8rem;
position: relative;
}
.poem-section h2::after,
.recognition-section h2::after {
content: "";
display: block;
width: 80px;
height: 4px;
background: linear-gradient(to right, #ff9ec0, #a8edea);
margin: 8px auto 0;
border-radius: 2px;
}
.poem-card {
background-color: white;
border-radius: 15px;
padding: 25px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
text-align: center;
margin-bottom: 25px;
border: 3px dashed #ffd1e0;
position: relative;
}
.poem-card::before {
content: "📜";
position: absolute;
top: -20px;
left: -15px;
font-size: 2rem;
transform: rotate(-20deg);
}
.poem-title {
color: #5a7bd3;
font-size: 1.7rem;
margin-bottom: 15px;
font-weight: bold;
}
.poem-author {
color: #ff9ec0;
font-size: 1.2rem;
margin-bottom: 25px;
font-style: italic;
}
.poem-content {
font-size: 1.4rem;
line-height: 2.2;
color: #5c5c5c;
}
.poem-content span {
display: block;
}
.voice-controls {
display: flex;
justify-content: center;
gap: 20px;
flex-wrap: wrap;
}
.btn {
padding: 16px 30px;
font-size: 1.2rem;
border: none;
border-radius: 50px;
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
font-weight: bold;
transition: all 0.3s ease;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.btn:active {
transform: translateY(3px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
}
.play-btn {
background: linear-gradient(to right, #ff9ec0, #ff6b9c);
color: white;
}
.play-btn:hover {
background: linear-gradient(to right, #ff8ab3, #ff5a8f);
transform: translateY(-3px);
}
.recognition-section {
flex: 1;
min-width: 300px;
background: linear-gradient(to bottom right, #e0f7ff, #ffebf3);
border-radius: 20px;
padding: 25px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
border: 4px solid #a8edea;
}
.recognition-box {
background-color: white;
border-radius: 15px;
padding: 20px;
min-height: 200px;
margin-bottom: 25px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
border: 3px solid #d0f0ff;
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
}
.recognition-box::before {
content: "🔊";
position: absolute;
bottom: -20px;
right: -15px;
font-size: 2.5rem;
transform: rotate(15deg);
}
.recognition-result {
font-size: 1.3rem;
color: #5c5c5c;
min-height: 150px;
/* display: flex; */
align-items: center;
justify-content: center;
text-align: center;
line-height: 1.8;
padding: 15px;
}
.recognition-result.active {
color: #ff6b9c;
font-weight: bold;
font-size: 1.4rem;
}
.recognition-controls {
display: flex;
justify-content: center;
gap: 20px;
margin-top: 15px;
}
.record-btn {
background: linear-gradient(to right, #5a7bd3, #3a5bb5);
color: white;
}
.record-btn:hover {
background: linear-gradient(to right, #4a6bc3, #2a4ba5);
transform: translateY(-3px);
}
.record-btn.recording {
background: linear-gradient(to right, #ff5252, #d32f2f);
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
.visualizer {
height: 60px;
background: linear-gradient(to bottom, #e0f7ff, #d0f0ff);
border-radius: 10px;
margin: 20px 0;
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
border: 2px solid #b8e8ff;
}
.visualizer-bars {
display: flex;
align-items: flex-end;
gap: 4px;
height: 100%;
width: 100%;
}
.bar {
width: 12px;
background: linear-gradient(to top, #ff9ec0, #ff6b9c);
border-radius: 6px 6px 0 0;
transition: height 0.2s;
}
.score-display {
text-align: center;
margin-top: 20px;
padding: 15px;
background: linear-gradient(to right, #a8edea, #b8e8ff);
border-radius: 15px;
font-size: 1.4rem;
color: #5a7bd3;
font-weight: bold;
display: none;
}
.score-display.show {
display: block;
animation: popIn 0.5s;
}
@keyframes popIn {
0% {
transform: scale(0.8);
opacity: 0;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.animals {
display: flex;
justify-content: space-around;
margin-top: 30px;
padding: 0 20px;
}
.animal {
font-size: 3.5rem;
text-align: center;
animation: bounce 2s infinite alternate;
}
.animal:nth-child(1) {
animation-delay: 0s;
}
.animal:nth-child(2) {
animation-delay: 0.5s;
}
.animal:nth-child(3) {
animation-delay: 1s;
}
@keyframes bounce {
0% {
transform: translateY(0);
}
100% {
transform: translateY(-20px);
}
}
footer {
text-align: center;
margin-top: 30px;
color: #5a7bd3;
font-size: 1rem;
padding: 10px;
}
.result-line {
display: block;
margin: 5px 0;
transition: all 0.3s;
}
.highlight {
background-color: #ffeb3b;
border-radius: 5px;
padding: 2px 5px;
animation: highlight 1s;
}
@keyframes highlight {
0% {
background-color: transparent;
}
50% {
background-color: #ffeb3b;
}
100% {
background-color: transparent;
}
}
.poem-hint {
font-size: 1.1rem;
color: #ff6b9c;
margin-top: 15px;
text-align: center;
font-weight: bold;
}
@media (max-width: 768px) {
.content-area {
flex-direction: column;
}
h1 {
font-size: 2.2rem;
}
.poem-content {
font-size: 1.2rem;
line-height: 2;
}
.btn {
padding: 14px 25px;
font-size: 1.1rem;
}
}
</style>
</head>
<body>
<!-- 装饰元素 -->
<div class="cloud"></div>
<div class="cloud"></div>
<div class="cloud"></div>
<div class="star"></div>
<div class="star"></div>
<div class="container">
<header>
<h1>童趣古诗学习乐园</h1>
<p class="subtitle">听古诗 • 读古诗 • 学古诗</p>
</header>
<div class="content-area">
<section class="poem-section">
<h2><i class="fas fa-book-open"></i> 古诗欣赏</h2>
<div class="poem-card">
<div class="poem-title">静夜思</div>
<div class="poem-author">【唐】李白</div>
<div class="poem-content" id="poemText">
<span id="line1">床前明月光,</span>
<span id="line2">疑是地上霜。</span>
<span id="line3">举头望明月,</span>
<span id="line4">低头思故乡。</span>
</div>
</div>
<div class="poem-hint">
请朗读上面这首诗,系统会识别并评分!
</div>
<div class="voice-controls">
<button id="playBtn" class="btn play-btn">
<i class="fas fa-volume-up"></i> 播放古诗
</button>
</div>
</section>
<section class="recognition-section">
<h2><i class="fas fa-microphone-alt"></i> 语音识别</h2>
<div class="recognition-box">
<div id="recognitionResult" class="recognition-result">
<div class="result-line">请点击下方按钮开始朗读古诗</div>
<div style="font-size: 2.5rem; margin-top: 15px;">🎤</div>
</div>
</div>
<div class="visualizer">
<div class="visualizer-bars" id="visualizer">
<!-- 动态生成的音频条 -->
</div>
</div>
<div class="recognition-controls">
<button id="recordBtn" class="btn record-btn">
<i class="fas fa-microphone"></i> 开始朗读
</button>
</div>
<div id="scoreDisplay" class="score-display">
匹配度: <span id="scoreValue">0</span>%
<div id="scoreFeedback" style="font-size: 1.1rem; margin-top: 8px;"></div>
</div>
</section>
</div>
<div class="animals">
<div class="animal">🐰</div>
<div class="animal">🐻</div>
<div class="animal">🐱</div>
</div>
<footer>
<p>使用浏览器内置语音功能 • 童趣学习 • 快乐成长</p>
</footer>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
// 创建音频可视化条
const visualizer = document.getElementById('visualizer');
for (let i = 0; i < 20; i++) {
const bar = document.createElement('div');
bar.className = 'bar';
bar.style.height = Math.floor(Math.random() * 10 + 5) + 'px';
visualizer.appendChild(bar);
}
// 获取DOM元素
const playBtn = document.getElementById('playBtn');
const recordBtn = document.getElementById('recordBtn');
const recognitionResult = document.getElementById('recognitionResult');
const scoreDisplay = document.getElementById('scoreDisplay');
const scoreValue = document.getElementById('scoreValue');
const scoreFeedback = document.getElementById('scoreFeedback');
const bars = document.querySelectorAll('.bar');
const poemLines = [
"床前明月光,",
"疑是地上霜。",
"举头望明月,",
"低头思故乡。"
];
// 初始化语音合成
const speechSynthesis = window.speechSynthesis;
let speechUtterance = null;
// 初始化语音识别
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
let recognition = null;
let isRecording = false;
let userPoem = [];
let currentLine = 0;
if (SpeechRecognition) {
recognition = new SpeechRecognition();
recognition.continuous = false;
recognition.lang = 'zh-CN';
recognition.interimResults = true;
recognition.onstart = function () {
isRecording = true;
recordBtn.classList.add('recording');
recordBtn.innerHTML = '<i class="fas fa-microphone"></i> 朗读中...';
recognitionResult.innerHTML = "<div class='result-line'>正在聆听...</div>";
scoreDisplay.classList.remove('show');
userPoem = [];
currentLine = 0;
// 开始动画
animateBars();
};
recognition.onresult = function (event) {
let transcript = '';
for (let i = event.resultIndex; i < event.results.length; i++) {
if (event.results[i].isFinal) {
transcript += event.results[i][0].transcript;
} else {
transcript = event.results[i][0].transcript;
}
}
// 更新显示
recognitionResult.innerHTML = '';
// poemLines.forEach((line, index) => {
// const lineEl = document.createElement('div');
// lineEl.className = 'result-line';
// lineEl.textContent = line;
// if (index < currentLine) {
// lineEl.style.color = '#5a7bd3';
// lineEl.style.fontWeight = 'bold';
// }
// recognitionResult.appendChild(lineEl);
// });
// 添加用户朗读文本
const userLine = document.createElement('div');
userLine.className = 'result-line';
userLine.style.color = '#ff6b9c';
userLine.style.fontWeight = 'bold';
userLine.textContent = transcript;
recognitionResult.appendChild(userLine);
// 保存用户朗读内容
if (event.results[event.results.length - 1].isFinal) {
userPoem.push(transcript);
highlightLine(currentLine);
currentLine++;
}
console.log("recognition.onresult", event.results, event.resultIndex, transcript, userPoem);
};
recognition.onerror = function (event) {
console.error('语音识别错误:', event.error);
recognitionResult.innerHTML =
"<div class='result-line'>识别错误,请重试</div><div style='font-size:2.5rem;margin-top:15px;'>😢</div>";
};
recognition.onend = function () {
isRecording = false;
recordBtn.classList.remove('recording');
recordBtn.innerHTML = '<i class="fas fa-microphone"></i> 开始朗读';
// 停止动画
stopBarsAnimation();
if (userPoem.length > 0) {
// 计算匹配度
calculateMatch(userPoem.join(''));
// 添加完成提示
const completeDiv = document.createElement('div');
completeDiv.className = 'result-line';
completeDiv.style.color = '#5a7bd3';
completeDiv.style.marginTop = '15px';
completeDiv.textContent = '朗读完成!';
recognitionResult.appendChild(completeDiv);
}
};
} else {
recordBtn.disabled = true;
recordBtn.innerHTML = '<i class="fas fa-exclamation-triangle"></i> 浏览器不支持';
recognitionResult.innerHTML =
"<div class='result-line'>您的浏览器不支持语音识别功能</div><div class='result-line'>请使用Chrome或Edge浏览器</div>";
}
// 播放古诗
playBtn.addEventListener('click', function () {
if (speechSynthesis.speaking) {
speechSynthesis.cancel();
playBtn.innerHTML = '<i class="fas fa-volume-up"></i> 播放古诗';
return;
}
const poemText = "静夜思,唐,李白。床前明月光,疑是地上霜。举头望明月,低头思故乡。";
speechUtterance = new SpeechSynthesisUtterance(poemText);
speechUtterance.lang = 'zh-CN';
speechUtterance.rate = 0.9;
speechUtterance.onstart = function () {
playBtn.innerHTML = '<i class="fas fa-stop"></i> 停止播放';
// 高亮显示每行诗句
let lineIndex = 0;
const highlightInterval = setInterval(() => {
if (lineIndex < poemLines.length) {
highlightLine(lineIndex);
lineIndex++;
} else {
clearInterval(highlightInterval);
}
}, 2000);
};
speechUtterance.onend = function () {
playBtn.innerHTML = '<i class="fas fa-volume-up"></i> 播放古诗';
};
speechSynthesis.speak(speechUtterance);
});
// 开始/停止朗读
recordBtn.addEventListener('click', function () {
iframePostMessage();
return
if (!recognition) return;
if (isRecording) {
recognition.stop();
} else {
recognition.start();
}
});
// 高亮显示指定行
function highlightLine(lineIndex) {
const lineId = `line${lineIndex + 1}`;
const lineElement = document.getElementById(lineId);
if (lineElement) {
lineElement.classList.add('highlight');
// 移除高亮效果
setTimeout(() => {
lineElement.classList.remove('highlight');
}, 2000);
}
}
// 计算匹配度
function calculateMatch(text) {
const targetText = "床前明月光疑是地上霜举头望明月低头思故乡";
const userText = text.replace(/\s+/g, '').replace(/[,。]/g, '');
if (!userText) return;
// 使用编辑距离算法计算相似度
const m = targetText.length;
const n = userText.length;
const dp = Array.from(Array(m + 1), () => Array(n + 1).fill(0));
for (let i = 0; i <= m; i++) {
dp[i][0] = i;
}
for (let j = 0; j <= n; j++) {
dp[0][j] = j;
}
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (targetText[i - 1] === userText[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(
dp[i - 1][j] + 1, // 删除
dp[i][j - 1] + 1, // 插入
dp[i - 1][j - 1] + 1 // 替换
);
}
}
}
const distance = dp[m][n];
const maxLength = Math.max(m, n);
const accuracy = Math.round((1 - distance / maxLength) * 100);
scoreValue.textContent = accuracy;
// 设置不同分数段的反馈
let feedback = '';
let color = '';
if (accuracy >= 90) {
feedback = '太棒了!读得非常准确!';
color = '#2ecc71';
} else if (accuracy >= 70) {
feedback = '很好!继续加油!';
color = '#f1c40f';
} else if (accuracy >= 50) {
feedback = '不错!再多练习几次!';
color = '#e67e22';
} else {
feedback = '再试一次吧!你可以的!';
color = '#e74c3c';
}
scoreFeedback.textContent = feedback;
scoreFeedback.style.color = color;
if (accuracy > 70) {
scoreDisplay.style.background = "linear-gradient(to right, #a8ffa8, #76e976)";
} else if (accuracy > 40) {
scoreDisplay.style.background = "linear-gradient(to right, #fff9a8, #ffe976)";
} else {
scoreDisplay.style.background = "linear-gradient(to right, #ffa8a8, #ff7676)";
}
scoreDisplay.classList.add('show');
}
// 音频条动画
let animationInterval;
function animateBars() {
animationInterval = setInterval(() => {
bars.forEach(bar => {
const newHeight = Math.floor(Math.random() * 40 + 10);
bar.style.height = newHeight + 'px';
});
}, 200);
}
function stopBarsAnimation() {
clearInterval(animationInterval);
bars.forEach(bar => {
bar.style.height = '5px';
});
}
// 添加装饰元素
createDecorations();
function createDecorations() {
const container = document.querySelector('.container');
for (let i = 0; i < 5; i++) {
const star = document.createElement('div');
star.className = 'star';
star.style.width = Math.floor(Math.random() * 15 + 10) + 'px';
star.style.height = star.style.width;
star.style.left = Math.random() * 100 + '%';
star.style.top = Math.random() * 100 + '%';
container.appendChild(star);
}
}
});
// 接收语音返回
let userPoem = [];
function speechResult(message) {
let status = message.status;
let data = message.data;
if (status == "start") {
} else if (status == "process") {
const recognitionResult = document.getElementById('recognitionResult');
let transcript = data;
userPoem = [data];
// 更新显示
recognitionResult.innerHTML = '';
// 添加用户朗读文本
const userLine = document.createElement('div');
userLine.className = 'result-line';
userLine.style.color = '#ff6b9c';
userLine.style.fontWeight = 'bold';
userLine.textContent = transcript;
recognitionResult.appendChild(userLine);
} else if (status == "end") {
calculateMatch(userPoem.join(''));
}
}
// 计算匹配度
function calculateMatch(text) {
const scoreDisplay = document.getElementById('scoreDisplay');
const scoreValue = document.getElementById('scoreValue');
const scoreFeedback = document.getElementById('scoreFeedback');
const targetText = "床前明月光疑是地上霜举头望明月低头思故乡";
const userText = text.replace(/\s+/g, '').replace(/[,。]/g, '');
if (!userText) return;
// 使用编辑距离算法计算相似度
const m = targetText.length;
const n = userText.length;
const dp = Array.from(Array(m + 1), () => Array(n + 1).fill(0));
for (let i = 0; i <= m; i++) {
dp[i][0] = i;
}
for (let j = 0; j <= n; j++) {
dp[0][j] = j;
}
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (targetText[i - 1] === userText[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(
dp[i - 1][j] + 1, // 删除
dp[i][j - 1] + 1, // 插入
dp[i - 1][j - 1] + 1 // 替换
);
}
}
}
const distance = dp[m][n];
const maxLength = Math.max(m, n);
const accuracy = Math.round((1 - distance / maxLength) * 100);
scoreValue.textContent = accuracy;
// 设置不同分数段的反馈
let feedback = '';
let color = '';
if (accuracy >= 90) {
feedback = '太棒了!读得非常准确!';
color = '#2ecc71';
} else if (accuracy >= 70) {
feedback = '很好!继续加油!';
color = '#f1c40f';
} else if (accuracy >= 50) {
feedback = '不错!再多练习几次!';
color = '#e67e22';
} else {
feedback = '再试一次吧!你可以的!';
color = '#e74c3c';
}
scoreFeedback.textContent = feedback;
scoreFeedback.style.color = color;
if (accuracy > 70) {
scoreDisplay.style.background = "linear-gradient(to right, #a8ffa8, #76e976)";
} else if (accuracy > 40) {
scoreDisplay.style.background = "linear-gradient(to right, #fff9a8, #ffe976)";
} else {
scoreDisplay.style.background = "linear-gradient(to right, #ffa8a8, #ff7676)";
}
scoreDisplay.classList.add('show');
}
// 网页间相互通信
window.addEventListener('message', function (event) {
// 检查消息来源是否可信
if (event.origin !== 'http://localhost') return;
const message = event.data;
// console.log('iframe 接收到的数据:', message);
if (message.type == "语音识别") {
speechResult(message);
}
else if (message.type == "体感识别") {
}
});
function iframePostMessage() {
const message = { type: "语音识别", status: "request" };
window.parent.postMessage(message, 'http://localhost'); // 替换为目标域名
}
</script>
</body>
</html>