[HTML] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>英语练习</title>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/xlsx/0.18.2/xlsx.full.min.js"></script>
<style>
/* 重置和基础样式 */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f5f7fa;
}
/* 容器样式 */
.container {
max-width: 800px;
margin: 40px auto;
padding: 0 20px;
}
/* 标题样式 */
h2 {
font-size: 24px;
font-weight: 500;
color: #2c3e50;
margin-bottom: 20px;
}
/* 文件输入框样式 */
.file-input-container {
margin-bottom: 30px;
}
#fileInput {
display: none;
}
.file-input-label {
display: inline-block;
padding: 12px 24px;
background-color: #3498db;
color: white;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.3s;
}
.file-input-label:hover {
background-color: #2980b9;
}
.file-type {
margin-top: 10px;
color: #666;
font-size: 14px;
}
/* 语音控制面板样式 */
.voice-controls {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
margin-bottom: 30px;
}
.voice-select {
margin-bottom: 20px;
}
.voice-select select {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 16px;
color: #2c3e50;
}
.slider-container {
margin: 15px 0;
}
.slider-container label {
display: block;
margin-bottom: 8px;
color: #2c3e50;
}
input[type="range"] {
width: 100%;
height: 6px;
background: #e0e0e0;
border-radius: 3px;
outline: none;
}
.slider-value {
display: inline-block;
min-width: 40px;
text-align: center;
margin-left: 10px;
color: #666;
}
/* 开始练习按钮样式 */
.view-modal-btn {
display: block;
width: 100%;
padding: 15px;
background-color: #2ecc71;
color: white;
border: none;
border-radius: 8px;
font-size: 18px;
cursor: pointer;
transition: background-color 0.3s;
margin: 30px 0;
}
.view-modal-btn:hover {
background-color: #27ae60;
}
/* Modal 样式优化 */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgb(0, 0, 0);
z-index: 1000;
}
/* 全屏时的样式 */
.modal.fullscreen {
width: 100vw;
height: 100vh;
}
.modal-content {
position: relative;
height: 100%;
overflow-y: scroll;
padding: 80px 40px 40px;
max-width: 1600px;
margin: 0 auto;
scrollbar-width: none;
-ms-overflow-style: none;
scroll-behavior: smooth;
}
.modal-content::-webkit-scrollbar {
display: none;
}
.modal-content::-webkit-scrollbar-track,
.modal-content::-webkit-scrollbar-thumb,
.modal-content::-webkit-scrollbar-thumb:hover {
display: none;
}
.close-btn {
position: fixed;
top: 30px;
right: 40px;
color: white;
font-size: 40px;
cursor: pointer;
background: none;
border: none;
padding: 15px;
z-index: 1001;
}
.sentence-pair {
background: rgba(255, 255, 255, 0.03);
border-radius: 16px;
padding: 30px;
margin-bottom: 30px;
display: flex;
align-items: flex-start;
transition: background-color 0.3s ease;
transform-origin: center;
}
.sentence-pair:hover {
background: rgba(255, 255, 255, 0.05);
}
.sentence-content {
flex: 1;
min-width: 0;
padding-right: 20px;
}
.english,
.chinese {
word-wrap: break-word;
/* 确保长文本会换行 */
}
/* 添加滚动条样式 */
.modal-content::-webkit-scrollbar {
width: 10px;
}
.modal-content::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
}
.modal-content::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 5px;
}
.modal-content::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
/* 播放按钮基础样式 */
.play-button {
width: 60px;
height: 60px;
border: none;
border-radius: 50%;
background-color: rgba(52, 152, 219, 0.9);
color: white;
cursor: pointer;
margin-right: 25px;
padding: 0;
position: relative;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(52, 152, 219, 0.3);
margin-top: 8px;
}
/* 播放图标容器 */
.play-button .icon-container {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
/* 播放三角形图标 */
.play-button .play-icon {
width: 0;
height: 0;
border-style: solid;
border-width: 18px 0 18px 27px;
border-color: transparent transparent transparent #ffffff;
margin-left: 4px;
}
/* 停止方块图标 */
.play-button .stop-icon {
width: 24px;
height: 24px;
background-color: #ffffff;
}
/* 加载动画 */
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.play-button .loading-icon {
width: 28px;
height: 28px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
/* 播放状态样式 */
.play-button.playing {
background-color: rgba(231, 76, 60, 0.9);
box-shadow: 0 2px 8px rgba(231, 76, 60, 0.3);
}
/* 悬停效果 */
.play-button:not(.loading):hover {
transform: scale(1.05);
}
.play-button:not(.loading):active {
transform: scale(0.95);
}
@keyframes ripple {
0% {
transform: scale(1);
opacity: 0.4;
}
100% {
transform: scale(1.5);
opacity: 0;
}
}
@keyframes pulse {
0% {
transform: scale(1);
opacity: 0.5;
}
50% {
transform: scale(1.1);
opacity: 0.2;
}
100% {
transform: scale(1);
opacity: 0.5;
}
}
.play-button.playing::before {
content: '';
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
border-radius: 50%;
border: 2px solid rgba(231, 76, 60, 0.5);
animation: pulse 2s ease-in-out infinite;
}
.play-button::after {
content: '';
position: absolute;
width: 100%;
height: 100%;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.2);
opacity: 0;
transform: scale(1);
pointer-events: none;
}
.play-button:active::after {
animation: ripple 0.6s ease-out;
}
.sentence-pair {
background: rgba(255, 255, 255, 0.03);
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
display: flex;
align-items: center;
transition: background-color 0.3s ease;
}
.sentence-pair:hover {
background: rgba(255, 255, 255, 0.05);
}
.english {
font-size: 50px;
line-height: 1.3;
color: #fff;
margin-bottom: 0;
transition: all 0.3s ease;
font-weight: 500;
opacity: 1;
font-family: 'HengShuiTi', sans-serif;
display: block;
}
.english.hidden {
display: none;
}
.chinese {
display: block;
font-size: 40px;
line-height: 1.3;
color: #f1c40f;
opacity: 0;
transition: all 0.3s ease;
visibility: hidden;
height: 0;
overflow: hidden;
}
.chinese.show {
opacity: 1;
visibility: visible;
height: auto;
}
.close-btn {
font-size: 36px;
color: #fff;
opacity: 0.8;
transition: opacity 0.3s;
}
.close-btn:hover {
opacity: 1;
}
/* 表格样式优化 */
table {
width: 100%;
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
margin-top: 30px;
}
th {
background-color: #f8f9fa;
color: #2c3e50;
font-weight: 500;
padding: 15px;
text-align: left;
}
td {
padding: 15px;
border-bottom: 1px solid #eee;
}
tr:last-child td {
border-bottom: none;
}
.file-options {
margin-top: 15px;
}
.checkbox-container {
display: flex;
align-items: center;
margin-bottom: 8px;
cursor: pointer;
}
.checkbox-container input[type="checkbox"] {
margin-right: 8px;
width: 16px;
height: 16px;
cursor: pointer;
}
.checkbox-label {
color: #666;
font-size: 14px;
}
.file-info {
display: none;
margin-top: 15px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
overflow: hidden;
}
.file-info-header {
padding: 15px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #eee;
}
.file-info-basic {
flex: 1;
}
.file-info-item {
color: #2c3e50;
margin: 4px 0;
font-size: 14px;
}
.file-info-item span {
color: #3498db;
font-weight: 500;
}
.preview-toggle {
display: flex;
align-items: center;
cursor: pointer;
padding: 8px 12px;
border-radius: 4px;
transition: background-color 0.2s;
}
.preview-toggle:hover {
background-color: #f5f7fa;
}
.preview-text {
color: #2c3e50;
font-size: 14px;
margin-right: 8px;
}
.preview-icon {
color: #666;
transition: transform 0.3s;
}
.preview-icon.open {
transform: rotate(180deg);
}
.preview-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
}
.preview-content.open {
max-height: 500px;
overflow-y: auto;
}
/* 修改表格样式 */
table {
width: 100%;
margin: 0;
border: none;
}
th {
background-color: #f8f9fa;
padding: 12px 15px;
font-weight: 500;
}
td {
padding: 12px 15px;
border-bottom: 1px solid #eee;
}
tr:last-child td {
border-bottom: none;
}
.progress-ring {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.progress-ring circle {
transform: rotate(-90deg);
transform-origin: 50% 50%;
stroke: #fff;
stroke-width: 2;
fill: none;
}
/* 加载动画样式 */
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.loading-icon {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: #fff;
animation: rotate 1s linear infinite;
}
/* 修改播放按钮相样式 */
.play-button {
position: relative;
width: 66px;
height: 66px;
border: none;
border-radius: 50%;
background-color: #3498db;
color: white;
cursor: pointer;
margin-right: 20px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.play-button.loading {
background-color: #7f8c8d;
cursor: wait;
}
.play-button.loading:hover {
transform: none;
box-shadow: 0 2px 6px rgba(52, 152, 219, 0.3);
}
@keyframes progress {
from {
stroke-dashoffset: var(--circumference);
}
to {
stroke-dashoffset: 0;
}
}
.progress-ring {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
.progress-ring circle {
transform: rotate(-90deg);
transform-origin: 50% 50%;
stroke: rgba(255, 255, 255, 0.8);
stroke-width: 2.5;
fill: none;
}
.progress-ring circle.animating {
animation: progress linear forwards;
animation-duration: var(--duration);
}
.progress-ring circle.paused {
animation-play-state: paused;
}
@keyframes playing-wave {
0% {
transform: scale(1);
opacity: 0.8;
}
50% {
transform: scale(1.2);
opacity: 0.4;
}
100% {
transform: scale(1);
opacity: 0.8;
}
}
.play-button {
position: relative;
width: 66px;
height: 66px;
border: none;
border-radius: 50%;
background-color: #3498db;
color: white;
cursor: pointer;
margin-right: 20px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
box-shadow: 0 2px 6px rgba(52, 152, 219, 0.3);
}
.play-button::after {
content: '';
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
border-radius: 50%;
border: 2px solid #3498db;
opacity: 0;
pointer-events: none;
}
.play-button.playing::after {
animation: playing-wave 2s ease-in-out infinite;
}
.play-button.playing {
background-color: #e74c3c;
box-shadow: 0 2px 6px rgba(231, 76, 60, 0.3);
}
.play-button.playing::after {
border-color: #e74c3c;
}
/* 添加重试状态样式 */
.play-button.retry {
background-color: #e74c3c;
animation: pulse 2s infinite;
}
.play-button .retry-icon {
width: 16px;
height: 16px;
border: 2px solid #ffffff;
border-radius: 50%;
position: relative;
}
.play-button .retry-icon::after {
content: '';
position: absolute;
top: 2px;
right: -2px;
width: 0;
height: 0;
border-style: solid;
border-width: 4px;
border-color: transparent transparent transparent #ffffff;
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
/* 添加句子激活状态的样式 */
.sentence-pair {
transition: all 0.3s ease;
transform-origin: center;
}
.sentence-pair.active {
transform: scale(1.02);
background: rgba(255, 255, 255, 0.08);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
/* 添加字体声明 */
@font-face {
font-family: 'HengShuiTi';
src: url('./heng-shui.ttf') format('truetype');
}
/* 修改预览文本样式 */
.font-size-preview {
background: rgb(0, 0, 0);
border-radius: 12px;
padding: 20px;
margin-top: 15px;
display: block;
}
.preview-text-en {
color: #fff;
font-family: 'HengShuiTi', sans-serif;
margin-bottom: 10px;
}
.preview-text-zh {
color: #f1c40f;
}
/* 连词成句样式 */
.word-arrange-container {
background: rgba(255, 255, 255, 0.03);
border-radius: 16px;
padding: 30px;
margin-bottom: 30px;
display: flex;
flex-direction: column;
transition: all 0.3s ease;
}
.word-arrange-container.active {
transform: scale(1.02);
background: rgba(255, 255, 255, 0.08);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.word-arrange-container.correct {
background: rgba(46, 204, 113, 0.2);
box-shadow: 0 4px 20px rgba(46, 204, 113, 0.3);
}
.word-arrange-container.incorrect {
background: rgba(231, 76, 60, 0.2);
box-shadow: 0 4px 20px rgba(231, 76, 60, 0.3);
}
.chinese-text {
color: #f1c40f;
font-size: 40px;
margin-bottom: 20px;
}
.word-bank {
display: flex;
flex-wrap: wrap;
margin-bottom: 20px;
min-height: 70px;
padding: 10px;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
}
.sentence-template {
display: flex;
flex-wrap: wrap;
min-height: 70px;
padding: 10px;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
align-items: center;
}
.draggable-word {
display: inline-flex;
padding: 2px 8px;
margin: 5px;
background: rgba(52, 152, 219, 0.7);
color: white;
border-radius: 4px;
cursor: move;
user-select: none;
transition: all 0.2s ease;
font-family: 'HengShuiTi', sans-serif;
}
.draggable-word:hover {
background: rgba(52, 152, 219, 0.9);
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.draggable-word.dragging {
opacity: 0.5;
background: rgba(52, 152, 219, 1);
}
.word-slot {
display: inline-flex;
height: 40px;
min-width: 50px;
margin: 5px;
background: rgba(255, 255, 255, 0.1);
border-bottom: 2px solid #3498db;
border-radius: 2px;
align-items: center;
justify-content: center;
}
.punctuation {
display: inline-flex;
color: white;
font-size: 26px;
margin: 5px;
padding: 8px 0;
align-items: center;
}
.feedback-message {
margin-top: 20px;
padding: 15px;
border-radius: 8px;
font-size: 24px;
text-align: center;
opacity: 0;
transition: all 0.3s ease;
height: 0;
overflow: hidden;
}
.feedback-message.correct {
background: rgba(46, 204, 113, 0.2);
color: #2ecc71;
opacity: 1;
height: auto;
padding: 15px;
}
.feedback-message.incorrect {
background: rgba(231, 76, 60, 0.2);
color: #e74c3c;
opacity: 1;
height: auto;
padding: 15px;
}
/* 添加干扰词样式 */
.draggable-word.distractor-word {
/* 移除特殊背景色,使用与普通单词相同的样式 */
background: rgba(52, 152, 219, 0.7);
}
.draggable-word.distractor-word:hover {
/* 移除特殊悬停背景色,使用与普通单词相同的样式 */
background: rgba(52, 152, 219, 0.9);
}
</style>
</head>
<body>
<div class="container">
<h2>英语学习助手</h2>
<div class="file-input-container">
<label for="fileInput" class="file-input-label">选择文件</label>
<input type="file" id="fileInput" accept=".csv,.xls,.xlsx">
<div class="file-options">
<label class="checkbox-container">
<input type="checkbox" id="skipHeader" checked>
<span class="checkbox-label">忽略标题行</span>
</label>
<div class="file-type">支持的格式:CSV, XLS, XLSX</div>
</div>
<div id="fileInfo" class="file-info">
<div class="file-info-header">
<div class="file-info-basic">
<div class="file-info-item">文件名:<span id="fileName">-</span></div>
<div class="file-info-item">总行数:<span id="totalRows">-</span></div>
<div class="file-info-item">有效行数:<span id="validRows">-</span></div>
</div>
<div class="preview-toggle">
<span class="preview-text">预览内容</span>
<span class="preview-icon">▼</span>
</div>
</div>
<div class="preview-content">
<table id="csvTable">
<thead>
<tr>
<th>英文</th>
<th>中文</th>
<th>干扰词</th>
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
</div>
</div>
</div>
<div class="voice-select" style="margin: 20px 0;">
<label for="voice">选择语音:</label>
<select id="voice" name="voice" style="padding: 5px; min-width: 200px;">
<option value="zh-CN-XiaoxiaoMultilingualNeural">晓晓 多语言</option>
<option value="zh-CN-XiaoxiaoNeural">晓晓</option>
<option value="zh-CN-YunxiNeural">云希</option>
<option value="zh-CN-YunjianNeural">云健</option>
<option value="zh-CN-XiaoyiNeural">晓伊</option>
<option value="zh-CN-YunyangNeural">云扬</option>
<option value="zh-CN-XiaochenNeural">晓辰</option>
<option value="zh-CN-XiaohanNeural">晓涵</option>
<option value="zh-CN-XiaomengNeural">晓梦</option>
<option value="zh-CN-XiaomoNeural">晓墨</option>
<option value="zh-CN-XiaoqiuNeural">晓秋</option>
<option value="zh-CN-XiaorouNeural">晓柔</option>
<option value="zh-CN-XiaoruiNeural">晓睿</option>
<option value="zh-CN-XiaoshuangNeural">晓双</option>
<option value="zh-CN-XiaoyanNeural">晓颜</option>
<option value="zh-CN-XiaoyouNeural">晓悠</option>
<option value="zh-CN-XiaozhenNeural">晓甄</option>
<option value="zh-CN-YunfengNeural">云枫</option>
<option value="zh-CN-YunhaoNeural">云皓</option>
<option value="zh-CN-YunjieNeural">云杰</option>
<option value="zh-CN-YunxiaNeural">云夏</option>
<option value="zh-CN-YunyeNeural">云野</option>
<option value="zh-CN-YunzeNeural">云泽</option>
<option value="zh-CN-YunfanMultilingualNeural">Yunfan 多语言</option>
<option value="zh-CN-YunxiaoMultilingualNeural">Yunxiao 多语言</option>
<option value="zh-CN-guangxi-YunqiNeural">云奇 广西</option>
<option value="zh-CN-henan-YundengNeural">云登</option>
<option value="zh-CN-liaoning-XiaobeiNeural">晓北 辽宁</option>
<option value="zh-CN-liaoning-YunbiaoNeural">云彪 辽宁</option>
<option value="zh-CN-shaanxi-XiaoniNeural">晓妮</option>
<option value="zh-CN-shandong-YunxiangNeural">云翔</option>
<option value="zh-CN-sichuan-YunxiNeural">云希 四川</option>
<option value="zh-HK-HiuMaanNeural">曉曼</option>
<option value="zh-HK-WanLungNeural">雲龍</option>
<option value="zh-HK-HiuGaaiNeural">曉佳</option>
<option value="zh-TW-HsiaoChenNeural">曉臻</option>
<option value="zh-TW-YunJheNeural">雲哲</option>
<option value="zh-TW-HsiaoYuNeural">曉雨</option>
</select>
</div>
<div class="voice-controls">
<div class="slider-container">
<label for="rate">语速:</label>
<input type="range" id="rate" min="-100" max="100" value="0">
<span class="slider-value" id="rateValue">0</span>
</div>
<div class="slider-container">
<label for="pitch">语调:</label>
<input type="range" id="pitch" min="-100" max="100" value="0">
<span class="slider-value" id="pitchValue">0</span>
</div>
<div class="slider-container">
<label for="fontSize">字体大小:</label>
<input type="range" id="fontSize" min="30" max="80" value="39">
<span class="slider-value" id="fontSizeValue">50px</span>
</div>
<div class="slider-container">
<label for="pronunciationType">发音类型:</label>
<select id="pronunciationType" style="width: 120px; padding: 5px; margin-left: 10px;">
<option value="2">美式发音</option>
<option value="1">英式发音</option>
</select>
</div>
<div class="font-size-preview">
<div class="preview-text-en">The quick brown fox jumps over the lazy dog.</div>
<div class="preview-text-zh">敏捷的棕色狐狸跳过了懒狗。</div>
</div>
</div>
<button class="view-modal-btn">开始练习</button>
<button class="view-modal-btn" style="background-color: #9b59b6;">连词成句</button>
</div>
<!-- 添加 Modal -->
<div id="fullscreenModal" class="modal">
<button class="close-btn">×</button>
<div class="modal-content" id="modalContent">
</div>
</div>
<!-- 连词成句 Modal -->
<div id="wordArrangeModal" class="modal">
<button class="close-btn">×</button>
<div class="modal-content" id="wordArrangeContent">
</div>
</div>
<script>
// 全局变量声明
let currentPlayingButton = null;
let currentPlayingAudio = null;
const audioElements = new Map(); // 存储所有音频元素
const audioCache = new Map(); // 添加音频缓存 Map
document.getElementById('fileInput').addEventListener('change', function (e) {
const file = e.target.files[0];
if (!file) {
document.getElementById('fileInfo').style.display = 'none';
return;
}
const reader = new FileReader();
if (file.name.endsWith('.csv')) {
reader.onload = function (event) {
const csvData = event.target.result;
processCSV(csvData);
};
reader.readAsText(file);
} else {
reader.onload = function (event) {
const data = new Uint8Array(event.target.result);
const workbook = XLSX.read(data, { type: 'array' });
processExcel(workbook);
};
reader.readAsArrayBuffer(file);
}
});
function processCSV(csvData) {
const rows = csvData.split('\n');
const tableBody = document.getElementById('tableBody');
tableBody.innerHTML = '';
const skipHeader = document.getElementById('skipHeader').checked;
const startIndex = skipHeader ? 1 : 0;
let validRowCount = 0;
rows.slice(startIndex).forEach(row => {
if (row.trim() === '') return;
const columns = row.match(/(".*?"|[^",]+)(?=\s*,|\s*$)/g);
if (columns && columns.length >= 2) {
const tr = document.createElement('tr');
const english = columns[0].replace(/^"|"$/g, '');
const chinese = columns[1].replace(/^"|"$/g, '');
const keywords = columns[2] ? columns[2].replace(/^"|"$/g, '').split(',').map(k => k.trim()) : [];
const distractors = columns[3] ? columns[3].replace(/^"|"$/g, '').split(',').map(d => d.trim()) : [];
// 处理英文文本的高亮,添加 font-weight: bold
let highlightedText = english;
if (keywords.length > 0) {
keywords.forEach(keyword => {
if (keyword) {
const regex = new RegExp(`(${keyword})`, 'gi');
highlightedText = highlightedText.replace(regex, '<span class="highlight-word" style="color: #ff4444; font-weight: bold; cursor: pointer;" data-word="$1">$1</span>');
}
});
}
tr.innerHTML = `
<td>${highlightedText}</td>
<td>${chinese}</td>
<td>${distractors.join(', ')}</td>
`;
tableBody.appendChild(tr);
validRowCount++;
}
});
const fileName = document.getElementById('fileInput').files[0].name;
updateFileInfo(fileName, rows.length, validRowCount);
}
function processExcel(workbook) {
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const data = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
const tableBody = document.getElementById('tableBody');
tableBody.innerHTML = '';
const skipHeader = document.getElementById('skipHeader').checked;
const startIndex = skipHeader ? 1 : 0;
let validRowCount = 0;
data.slice(startIndex).forEach(row => {
if (row.length >= 2) {
const tr = document.createElement('tr');
const english = row[0];
const chinese = row[1];
const keywords = row[2] ? row[2].split(',').map(k => k.trim()) : [];
const distractors = row[3] ? row[3].split(',').map(d => d.trim()) : [];
// 处理英文文本的高亮,添加 font-weight: bold
let highlightedText = english;
if (keywords.length > 0) {
keywords.forEach(keyword => {
if (keyword) {
const regex = new RegExp(`(${keyword})`, 'gi');
highlightedText = highlightedText.replace(regex, '<span class="highlight-word" style="color: #ff4444; font-weight: bold; cursor: pointer;" data-word="$1">$1</span>');
}
});
}
tr.innerHTML = `
<td>${highlightedText}</td>
<td>${chinese}</td>
<td>${distractors.join(', ')}</td>
`;
tableBody.appendChild(tr);
validRowCount++;
}
});
const fileName = document.getElementById('fileInput').files[0].name;
updateFileInfo(fileName, data.length, validRowCount);
}
// 添加滑值显示更新
document.getElementById('rate').addEventListener('input', function () {
document.getElementById('rateValue').textContent = this.value;
});
document.getElementById('pitch').addEventListener('input', function () {
document.getElementById('pitchValue').textContent = this.value;
});
function getRandom32BitString() {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < 32; i++) {
const randomIndex = Math.floor(Math.random() * characters.length);
result += characters[randomIndex];
}
return result;
}
async function fetchAndCacheAudio(url) {
const response = await fetch(url, {
method: 'GET',
mode: 'cors', // 尝试 cors 模式
credentials: 'omit',
headers: {
'Accept': 'audio/mpeg',
}
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
// 如果上面的方式不行,尝试使用 XMLHttpRequest
if (!response.body) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'blob';
xhr.onload = function () {
if (this.status === 200) {
const blob = new Blob([this.response], { type: 'audio/mpeg' });
const blobUrl = URL.createObjectURL(blob);
resolve(blobUrl);
} else {
reject(new Error('XHR request failed'));
}
};
xhr.onerror = function () {
reject(new Error('XHR request failed'));
};
xhr.send();
});
}
const audioBlob = await response.blob();
const blobUrl = URL.createObjectURL(audioBlob);
return blobUrl;
}
// 用于生成缓存键的函数
function generateCacheKey(text, voice, rate, pitch) {
return `${text}_${voice}_${rate}_${pitch}`;
}
function createPlayButton(text) {
const button = document.createElement('button');
button.className = 'play-button';
// 创建图标容器
const iconContainer = document.createElement('div');
iconContainer.className = 'icon-container';
const playIcon = document.createElement('div');
playIcon.className = 'play-icon';
iconContainer.appendChild(playIcon);
button.appendChild(iconContainer);
// 创建音频元素
const audio = document.createElement('audio');
audio.preload = 'auto';
let isLoading = false;
let isLoaded = false;
// 定义 stopPlaying 函数在 createPlayButton 函数内部
function stopPlaying() {
if (audio) {
audio.pause();
audio.currentTime = 0;
}
button.classList.remove('playing');
iconContainer.innerHTML = '<div class="play-icon"></div>';
// 停止播放时移除激活状态
const sentenceDiv = button.closest('.sentence-pair');
if (sentenceDiv) {
sentenceDiv.classList.remove('active');
}
if (currentPlayingButton === button) {
currentPlayingButton = null;
currentPlayingAudio = null;
}
}
async function loadAudio() {
if (isLoading || isLoaded) return;
isLoading = true;
button.classList.add('loading');
iconContainer.innerHTML = '<div class="loading-icon"></div>';
try {
const voice = document.getElementById('voice').value;
const rate = document.getElementById('rate').value;
const pitch = document.getElementById('pitch').value;
const encodedText = encodeURIComponent(text);
const cacheKey = btoa(`${text}_${voice}_${rate}_${pitch}`).replace(/[+/=]/g, '');
const url = `https://t.leftsite.cn/tts?t=${encodedText}&v=${voice}&r=${rate}&p=${pitch}&o=audio-24khz-48kbitrate-mono-mp3&cache=${cacheKey}`;
audio.src = url;
await new Promise((resolve, reject) => {
const loadHandler = () => {
resolve();
cleanup();
};
const errorHandler = () => {
reject(new Error('Failed to load audio'));
cleanup();
};
const cleanup = () => {
audio.removeEventListener('canplaythrough', loadHandler);
audio.removeEventListener('error', errorHandler);
};
audio.addEventListener('canplaythrough', loadHandler, { once: true });
audio.addEventListener('error', errorHandler, { once: true });
});
isLoaded = true;
return true;
} catch (error) {
console.error('Error loading audio:', error);
return false;
} finally {
isLoading = false;
button.classList.remove('loading');
iconContainer.innerHTML = '<div class="play-icon"></div>';
}
}
button.loadAudio = loadAudio;
button.addEventListener('click', async function (e) {
e.stopPropagation();
// 获取当前句子元素并激活
const sentenceDiv = this.closest('.sentence-pair');
activateSentence(sentenceDiv);
if (this === currentPlayingButton) {
stopPlaying();
return;
}
// 停止其他正在播放的音频
if (currentPlayingButton && currentPlayingButton.stopPlaying) {
currentPlayingButton.stopPlaying();
}
// 如果音频未加载,先加载
if (!isLoaded) {
const success = await loadAudio();
if (!success) return;
}
try {
// 设置播放状态
this.classList.add('playing');
iconContainer.innerHTML = '<div class="stop-icon"></div>';
// 设置播放结束的处理
audio.onended = () => {
stopPlaying();
// 播放结束时移除激活状态
sentenceDiv.classList.remove('active');
};
// 开始播放
currentPlayingButton = this;
currentPlayingAudio = audio;
await audio.play();
} catch (error) {
console.error('Error playing audio:', error);
this.classList.remove('playing');
iconContainer.innerHTML = '<div class="play-icon"></div>';
// 出错时移除激活状态
sentenceDiv.classList.remove('active');
}
});
// 将 stopPlaying 函数附加到按钮上
button.stopPlaying = stopPlaying;
return button;
}
async function showModal() {
const modal = document.getElementById('fullscreenModal');
const modalContent = document.getElementById('modalContent');
modalContent.innerHTML = '';
// 获取当前设置的字体大小
const fontSize = document.getElementById('fontSize').value;
// 获取表格中的所有行
const rows = document.getElementById('tableBody').getElementsByTagName('tr');
if (rows.length === 0) {
modalContent.innerHTML = '<div style="color: white; text-align: center;">请先选择文件</div>';
modal.style.display = 'block';
return;
}
// 创建所有按钮和内容
Array.from(rows).forEach((row, index) => {
const english = row.cells[0].innerHTML;
const chinese = row.cells[1].textContent;
const keywords = row.cells[2] ? row.cells[2].textContent.split(',').map(k => k.trim()) : [];
const sentenceDiv = document.createElement('div');
sentenceDiv.className = 'sentence-pair';
const contentDiv = document.createElement('div');
contentDiv.className = 'sentence-content';
const englishDiv = document.createElement('div');
englishDiv.className = 'english';
englishDiv.innerHTML = english;
englishDiv.style.fontSize = `${fontSize}px`;
const chineseDiv = document.createElement('div');
chineseDiv.className = 'chinese';
chineseDiv.textContent = chinese;
chineseDiv.style.fontSize = `${fontSize}px`;
contentDiv.appendChild(englishDiv);
contentDiv.appendChild(chineseDiv);
const playButton = createPlayButton(english.replace(/<[^>]*>/g, ''));
sentenceDiv.appendChild(playButton);
sentenceDiv.appendChild(contentDiv);
// 修改高亮词点击事件处理
contentDiv.querySelectorAll('.highlight-word').forEach(span => {
span.addEventListener('click', async function (e) {
e.stopPropagation(); // 阻止事件冒泡到父元素
e.preventDefault(); // 阻止默认行为
const word = this.dataset.word;
// 停止当前正在播放的音频
if (currentPlayingButton && currentPlayingButton.stopPlaying) {
currentPlayingButton.stopPlaying();
}
// 创建音频元素并设置参数
const voice = document.getElementById('voice').value;
const rate = document.getElementById('rate').value;
const pitch = document.getElementById('pitch').value;
const encodedText = encodeURIComponent(word);
const cacheKey = btoa(`${word}_${voice}_${rate}_${pitch}`).replace(/[+/=]/g, '');
const url = `https://t.leftsite.cn/tts?t=${encodedText}&v=${voice}&r=${rate}&p=${pitch}&o=audio-24khz-48kbitrate-mono-mp3&cache=${cacheKey}`;
const audio = new Audio(url);
audio.preload = 'auto';
try {
// 播放音频
await audio.play();
// 高亮动画效果
this.style.transition = 'all 0.3s';
this.style.backgroundColor = 'rgba(255, 68, 68, 0.2)';
setTimeout(() => {
this.style.backgroundColor = 'transparent';
}, 300);
} catch (error) {
console.error('Error playing audio:', error);
}
});
});
// 修改 contentDiv 的点击事件处理,排除高亮词的点击
contentDiv.addEventListener('click', function (e) {
// 如果点击的是高亮词,不执行切换操作
if (e.target.classList.contains('highlight-word')) {
return;
}
const sentenceDiv = this.closest('.sentence-pair');
activateSentence(sentenceDiv);
const english = this.querySelector('.english');
const chinese = this.querySelector('.chinese');
if (!chinese.classList.contains('show')) {
chinese.classList.add('show');
english.classList.add('hidden');
} else {
chinese.classList.remove('show');
english.classList.remove('hidden');
}
e.stopPropagation();
});
modalContent.appendChild(sentenceDiv);
});
modal.style.display = 'block';
// 请求全屏
try {
await modal.requestFullscreen();
// 添加全屏状态类
modal.classList.add('fullscreen');
} catch (err) {
console.warn('Failed to enter fullscreen:', err);
}
// 自动加载所有音频
requestAnimationFrame(() => {
const buttons = document.querySelectorAll('.play-button');
buttons.forEach((button, index) => {
if (button.loadAudio) {
// 错开加载时间,避免同时发起太多请求
setTimeout(() => {
button.loadAudio();
}, index * 200); // 每个音频间隔 200ms 开始加载
}
});
});
}
// 在语音设置改变时更新所有音频源
function updateAllAudioSources() {
audioElements.forEach(({ audio, text }) => {
const voice = document.getElementById('voice').value;
const rate = document.getElementById('rate').value;
const pitch = document.getElementById('pitch').value;
const encodedText = encodeURIComponent(text);
const cacheKey = btoa(`${text}_${voice}_${rate}_${pitch}`).replace(/[+/=]/g, '');
const url = `https://t.leftsite.cn/tts?t=${encodedText}&v=${voice}&r=${rate}&p=${pitch}&o=audio-24khz-48kbitrate-mono-mp3&cache=${cacheKey}`;
audio.src = url;
audio.load();
});
}
// 添加语音设置变化监听
document.getElementById('voice').addEventListener('change', updateAllAudioSources);
document.getElementById('rate').addEventListener('change', updateAllAudioSources);
document.getElementById('pitch').addEventListener('change', updateAllAudioSources);
// 修改 hideModal 函数
async function hideModal() {
const modal = document.getElementById('fullscreenModal');
// 如果处于全屏状态,退出全屏
if (document.fullscreenElement) {
try {
await document.exitFullscreen();
} catch (err) {
console.warn('Failed to exit fullscreen:', err);
}
}
// 移除全屏状态类
modal.classList.remove('fullscreen');
modal.style.display = 'none';
// 消所有加载中的请求
if (currentPlayingButton && currentPlayingButton.cancelLoading) {
currentPlayingButton.cancelLoading();
}
// 停止当前播放的音频
if (currentPlayingButton && currentPlayingButton.stopPlaying) {
currentPlayingButton.stopPlaying();
}
currentPlayingButton = null;
currentPlayingAudio = null;
// 重置所有句子为初始状态
document.querySelectorAll('.sentence-pair').forEach(pair => {
const contentDiv = pair.querySelector('.sentence-content');
const english = contentDiv.querySelector('.english');
const chinese = contentDiv.querySelector('.chinese');
chinese.classList.remove('show');
english.classList.remove('hidden');
contentDiv.insertBefore(english, chinese);
// 重置播放按钮状态
const playButton = pair.querySelector('.play-button');
playButton.classList.remove('playing', 'loading');
playButton.innerHTML = '▶';
});
}
// 监听全屏状态变化
document.addEventListener('fullscreenchange', function () {
const modal = document.getElementById('fullscreenModal');
if (!document.fullscreenElement && modal.style.display === 'block') {
// 如果退出全屏但 modal 仍在显示,则关闭 modal
hideModal();
}
});
// 修改预览切换函数
function togglePreview() {
const content = document.querySelector('.preview-content');
const icon = document.querySelector('.preview-icon');
content.classList.toggle('open');
icon.classList.toggle('open');
if (content.classList.contains('open')) {
const tableHeight = document.getElementById('csvTable').offsetHeight;
content.style.maxHeight = Math.min(500, tableHeight + 40) + 'px';
} else {
content.style.maxHeight = '0';
}
}
// 修改文件信息更新函数
function updateFileInfo(fileName, totalRows, validRows) {
const fileInfo = document.getElementById('fileInfo');
document.getElementById('fileName').textContent = fileName;
document.getElementById('totalRows').textContent = totalRows;
document.getElementById('validRows').textContent = validRows;
if (validRows > 0) {
fileInfo.style.display = 'block';
} else {
fileInfo.style.display = 'none';
}
// 确保预览内容是折叠的
const content = document.querySelector('.preview-content');
const icon = document.querySelector('.preview-icon');
content.classList.remove('open');
icon.classList.remove('open');
content.style.maxHeight = '0';
}
// 修改文件选择处理
document.getElementById('fileInput').addEventListener('click', function (e) {
const fileInfo = document.getElementById('fileInfo');
fileInfo.style.display = 'none';
document.getElementById('fileName').textContent = '-';
document.getElementById('totalRows').textContent = '-';
document.getElementById('validRows').textContent = '-';
});
// 在语音设置改变时清除所有音频元素的缓存
document.getElementById('voice').addEventListener('change', () => {
document.querySelectorAll('audio').forEach(audio => {
audio.removeAttribute('src');
});
});
document.getElementById('rate').addEventListener('change', () => {
document.querySelectorAll('audio').forEach(audio => {
audio.removeAttribute('src');
});
});
document.getElementById('pitch').addEventListener('change', () => {
document.querySelectorAll('audio').forEach(audio => {
audio.removeAttribute('src');
});
});
// 修改 hideModal 函数,确保清理 Blob URLs
window.addEventListener('beforeunload', () => {
// 清除所有缓存的音频资源
audioElements.forEach(({ audio }) => {
if (audio.src) {
audio.src = '';
}
});
audioElements.clear();
});
// 添加激句子的函数
function activateSentence(sentenceDiv) {
// 移除其他句子的激活状态
document.querySelectorAll('.sentence-pair.active').forEach(div => {
if (div !== sentenceDiv) {
div.classList.remove('active');
}
});
// 添加当前句子的激活状态
sentenceDiv.classList.add('active');
// 计算滚动位置
const modalContent = document.querySelector('.modal-content');
const modalHeight = modalContent.clientHeight;
const sentenceRect = sentenceDiv.getBoundingClientRect();
const modalRect = modalContent.getBoundingClientRect();
const relativeTop = sentenceRect.top - modalRect.top;
const targetScroll = modalContent.scrollTop + relativeTop - (modalHeight / 2) + (sentenceRect.height / 2);
// 平滑滚动到目标位置
modalContent.scrollTo({
top: targetScroll,
behavior: 'smooth'
});
}
// 添加点击其他区域取消激活状态的处理
document.querySelector('.modal-content').addEventListener('click', function (e) {
if (e.target === this) {
document.querySelectorAll('.sentence-pair.active').forEach(div => {
div.classList.remove('active');
});
}
});
// 字体大小调整相关代码
const fontSizeSlider = document.getElementById('fontSize');
const fontSizeValue = document.getElementById('fontSizeValue');
const preview = document.querySelector('.font-size-preview');
const previewEn = document.querySelector('.preview-text-en');
const previewZh = document.querySelector('.preview-text-zh');
// 初始化预览文本字体大小
const initialFontSize = fontSizeSlider.value;
previewEn.style.fontSize = `${initialFontSize}px`;
previewZh.style.fontSize = `${initialFontSize}px`;
fontSizeValue.textContent = `${initialFontSize}px`;
// 移除鼠标悬浮相关的事件监听
// 更新字体大小
fontSizeSlider.addEventListener('input', function () {
const size = this.value;
fontSizeValue.textContent = `${size}px`;
// 更新预览文本大小
previewEn.style.fontSize = `${size}px`;
previewZh.style.fontSize = `${size}px`;
// 更新modal中所有文本的大小
if (document.getElementById('fullscreenModal').style.display === 'block') {
document.querySelectorAll('.english, .chinese').forEach(element => {
element.style.fontSize = `${size}px`;
});
}
});
// ---------- 连词成句功能 ----------
// 存储原始句子信息的数组
let originalSentences = [];
// 显示连词成句的 Modal
async function showWordArrangeModal() {
const modal = document.getElementById('wordArrangeModal');
const modalContent = document.getElementById('wordArrangeContent');
modalContent.innerHTML = '';
// 获取表格中的所有行
const rows = document.getElementById('tableBody').getElementsByTagName('tr');
if (rows.length === 0) {
modalContent.innerHTML = '<div style="color: white; text-align: center;">请先选择文件</div>';
modal.style.display = 'block';
return;
}
// 清空原始句子数组
originalSentences = [];
// 获取当前设置的字体大小
const fontSize = document.getElementById('fontSize').value;
// 为每一行创建一个连词成句的容器
Array.from(rows).forEach((row, index) => {
// 获取英文和中文文本
const english = row.cells[0].textContent.replace(/<[^>]*>/g, '');
const chinese = row.cells[1].textContent;
const distractors = row.cells[2] ? row.cells[2].textContent.split(',').filter(d => d.trim() !== '') : [];
// 存储原始句子
originalSentences.push({
english: english,
chinese: chinese,
distractors: distractors,
index: index
});
// 创建连词成句容器
const container = document.createElement('div');
container.className = 'word-arrange-container';
container.dataset.index = index;
// 创建中文提示
const chineseText = document.createElement('div');
chineseText.className = 'chinese-text';
chineseText.textContent = chinese;
chineseText.style.fontSize = `${fontSize}px`;
chineseText.addEventListener('click', function() {
if (this.dataset.showingEnglish) {
this.textContent = chinese;
this.dataset.showingEnglish = '';
} else {
this.textContent = english;
this.dataset.showingEnglish = 'true';
}
});
container.appendChild(chineseText);
// 创建单词银行区域
const wordBank = document.createElement('div');
wordBank.className = 'word-bank';
wordBank.id = `word-bank-${index}`;
container.appendChild(wordBank);
// 创建句子模板区域
const sentenceTemplate = document.createElement('div');
sentenceTemplate.className = 'sentence-template';
sentenceTemplate.id = `sentence-template-${index}`;
container.appendChild(sentenceTemplate);
// 添加反馈信息区域
const feedback = document.createElement('div');
feedback.className = 'feedback-message';
feedback.id = `feedback-${index}`;
container.appendChild(feedback);
// 将容器添加到 Modal 内容中
modalContent.appendChild(container);
// 处理英文句子,抽取单词和标点,并添加干扰词
processEnglishSentence(english, index, distractors);
});
// 显示 Modal
modal.style.display = 'block';
// 请求全屏
try {
await modal.requestFullscreen();
modal.classList.add('fullscreen');
} catch (err) {
console.warn('Failed to enter fullscreen:', err);
}
// 初始化拖放功能
initDragAndDrop();
}
// 处理英文句子,抽取单词和标点
function processEnglishSentence(sentence, index, distractors = []) {
// 拆分句子为单词和标点
const pattern = /(\w+[-']\w+|\w+|[,.!?;:"""''])/g;
const tokens = sentence.match(pattern) || [];
// 跟踪当前处理的位置
let position = 0;
// 收集单词和标点
const words = [];
const punctuations = [];
const structure = [];
tokens.forEach(token => {
// 查找token在原句中的位置,并检查前后是否有空格
const tokenPos = sentence.indexOf(token, position);
const hasPrefixSpace = tokenPos > position;
position = tokenPos + token.length;
// 检查是否为标点
if (/^[,.!?;:"""'']$/.test(token)) {
punctuations.push({
text: token,
prefixSpace: hasPrefixSpace
});
structure.push({
type: 'punctuation',
text: token,
prefixSpace: hasPrefixSpace
});
} else {
words.push({
text: token,
prefixSpace: hasPrefixSpace
});
structure.push({
type: 'word',
prefixSpace: hasPrefixSpace
});
}
});
// 添加干扰词
distractors.forEach(distractor => {
if (distractor && distractor.trim() !== '') {
words.push({
text: distractor.trim(),
prefixSpace: false,
isDistractor: true
});
}
});
// 随机打乱单词顺序
shuffleArray(words);
// 填充单词银行
const wordBank = document.getElementById(`word-bank-${index}`);
words.forEach(word => {
const wordElement = createDraggableWord(word.text);
// 移除为干扰词添加特殊样式的代码
// if (word.isDistractor) {
// wordElement.classList.add('distractor-word');
// }
wordBank.appendChild(wordElement);
});
// 创建句子模板
const sentenceTemplate = document.getElementById(`sentence-template-${index}`);
structure.forEach(item => {
if (item.prefixSpace) {
sentenceTemplate.appendChild(document.createTextNode(' '));
}
if (item.type === 'word') {
const slot = document.createElement('div');
slot.className = 'word-slot';
sentenceTemplate.appendChild(slot);
} else {
const punctuation = document.createElement('span');
punctuation.className = 'punctuation';
punctuation.textContent = item.text;
sentenceTemplate.appendChild(punctuation);
}
});
}
// 创建可拖动的单词元素
function createDraggableWord(text) {
const word = document.createElement('div');
word.className = 'draggable-word word';
word.textContent = text;
word.draggable = true;
word.style.fontSize = document.getElementById('fontSize').value + 'px'; // 改为读取当前滑块值
return word;
// 获取当前字体大小设置并应用
const fontSize = document.getElementById('fontSize').value;
word.style.fontSize = `${fontSize}px`;
return word;
}
// 随机打乱数组
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
// 初始化拖放功能
function initDragAndDrop() {
// 获取所有可拖动单词和目标槽
const draggableWords = document.querySelectorAll('.draggable-word');
const wordSlots = document.querySelectorAll('.word-slot');
draggableWords.forEach(word => {
// 拖动开始事件
word.addEventListener('dragstart', function (e) {
this.classList.add('dragging');
e.dataTransfer.setData('text/plain', this.textContent);
// 记录源容器的索引
const container = this.closest('.word-arrange-container');
if (container) {
e.dataTransfer.setData('sourceContainerIndex', container.dataset.index);
}
// 如果单词在单词槽中,记录父元素信息
if (this.parentElement.classList.contains('word-slot')) {
e.dataTransfer.setData('source-slot', 'true');
} else if (this.parentElement.classList.contains('word-bank')) {
// 添加标记表示来源是单词银行
e.dataTransfer.setData('source-bank', 'true');
}
});
// 拖动结束事件
word.addEventListener('dragend', function () {
this.classList.remove('dragging');
});
});
// 对所有单词槽添加拖放事件
wordSlots.forEach(slot => {
// 拖动进入槽区域
slot.addEventListener('dragover', function (e) {
e.preventDefault();
this.style.background = 'rgba(255, 255, 255, 0.2)';
});
// 拖动离开槽区域
slot.addEventListener('dragleave', function () {
this.style.background = 'rgba(255, 255, 255, 0.1)';
});
// 拖动放置事件
slot.addEventListener('drop', function (e) {
e.preventDefault();
this.style.background = 'rgba(255, 255, 255, 0.1)';
// 检查是否在同一个句子组内
const targetContainer = this.closest('.word-arrange-container');
const sourceContainerIndex = e.dataTransfer.getData('sourceContainerIndex');
if (targetContainer && sourceContainerIndex &&
targetContainer.dataset.index !== sourceContainerIndex) {
return; // 如果不是同一组,则不允许放置
}
const wordText = e.dataTransfer.getData('text/plain');
const sourceIsSlot = e.dataTransfer.getData('source-slot') === 'true';
const sourceIsBank = e.dataTransfer.getData('source-bank') === 'true';
// 如果当前槽已有单词
if (this.querySelector('.draggable-word')) {
// 如果源也是单词槽,则交换单词
if (sourceIsSlot) {
const draggingWord = document.querySelector('.dragging');
if (draggingWord && draggingWord.parentElement !== this) {
const existingWord = this.querySelector('.draggable-word');
const sourceSlot = draggingWord.parentElement;
// 交换单词
sourceSlot.appendChild(existingWord);
this.appendChild(draggingWord);
// 检查是否是句子的第一个槽位
const isFirstSlot = Array.from(this.parentElement.children).indexOf(this) === 0;
if (isFirstSlot) {
draggingWord.textContent = draggingWord.textContent.charAt(0).toUpperCase() +
draggingWord.textContent.slice(1);
}
// 验证当前句子
validateSentence(this.closest('.word-arrange-container'));
}
} else if (sourceIsBank) {
// 如果源是单词银行,将当前槽的单词放回单词银行,新单词放入槽
const draggingWord = document.querySelector('.dragging');
if (draggingWord) {
const existingWord = this.querySelector('.draggable-word');
const container = this.closest('.word-arrange-container');
const wordBank = container.querySelector('.word-bank');
// 将现有单词放回单词银行
wordBank.appendChild(existingWord);
// 将新单词放入槽位
this.appendChild(draggingWord);
// 检查是否是句子的第一个槽位
const isFirstSlot = Array.from(this.parentElement.children).indexOf(this) === 0;
if (isFirstSlot) {
draggingWord.textContent = draggingWord.textContent.charAt(0).toUpperCase() +
draggingWord.textContent.slice(1);
}
// 验证当前句子
validateSentence(container);
}
}
} else {
// 当前槽没有单词,直接添加
const draggingWord = document.querySelector('.dragging');
if (draggingWord) {
this.appendChild(draggingWord);
// 检查是否是句子的第一个槽位
const isFirstSlot = Array.from(this.parentElement.children).indexOf(this) === 0;
if (isFirstSlot) {
draggingWord.textContent = draggingWord.textContent.charAt(0).toUpperCase() +
draggingWord.textContent.slice(1);
}
// 验证当前句子
validateSentence(this.closest('.word-arrange-container'));
} else {
// 创建新单词元素(可能来自外部拖拽)
const newWord = createDraggableWord(wordText);
newWord.draggable = true;
this.appendChild(newWord);
// 检查是否是句子的第一个槽位
const isFirstSlot = Array.from(this.parentElement.children).indexOf(this) === 0;
if (isFirstSlot) {
newWord.textContent = newWord.textContent.charAt(0).toUpperCase() +
newWord.textContent.slice(1);
}
// 为新创建的单词添加拖拽事件
initWordDragEvents(newWord);
// 如果源是单词银行,移除源单词
if (!sourceIsSlot) {
const sourceWord = document.querySelector(`.draggable-word:not(.dragging):contains('${wordText}')`);
if (sourceWord && sourceWord.parentElement.classList.contains('word-bank')) {
sourceWord.remove();
}
}
// 验证当前句子
validateSentence(this.closest('.word-arrange-container'));
}
}
});
});
// 对单词银行区域添加拖放事件
document.querySelectorAll('.word-bank').forEach(bank => {
bank.addEventListener('dragover', function (e) {
e.preventDefault();
});
bank.addEventListener('drop', function (e) {
e.preventDefault();
// 检查是否在同一个句子组内
const targetContainer = this.closest('.word-arrange-container');
const sourceContainerIndex = e.dataTransfer.getData('sourceContainerIndex');
if (targetContainer && sourceContainerIndex &&
targetContainer.dataset.index !== sourceContainerIndex) {
return; // 如果不是同一组,则不允许放置
}
const draggingWord = document.querySelector('.dragging');
if (draggingWord && draggingWord.parentElement.classList.contains('word-slot')) {
bank.appendChild(draggingWord);
// 验证关联的句子
const containerIndex = bank.id.split('-').pop();
const container = document.querySelector(`.word-arrange-container[data-index="${containerIndex}"]`);
validateSentence(container);
}
});
});
}
// 验证句子是否正确组装
function validateSentence(container) {
if (!container) return;
const index = container.dataset.index;
const originalSentence = originalSentences[index];
const feedback = document.getElementById(`feedback-${index}`);
// 获取所有单词槽
const wordSlots = container.querySelectorAll('.word-slot');
// 检查所有槽位是否都填充了单词
let allSlotsFilled = true;
wordSlots.forEach(slot => {
if (!slot.querySelector('.draggable-word')) {
allSlotsFilled = false;
}
});
// 如果并非所有槽都已填充,清除反馈
if (!allSlotsFilled) {
feedback.className = 'feedback-message';
container.classList.remove('correct', 'incorrect');
return;
}
// 重构用户组装的句子
let reconstructedSentence = '';
const sentenceTemplate = container.querySelector('.sentence-template');
// 遍历句子模板的所有子节点
for (let i = 0; i < sentenceTemplate.childNodes.length; i++) {
const node = sentenceTemplate.childNodes[i];
if (node.nodeType === Node.TEXT_NODE) {
// 如果是文本节点(空格),添加到重构句子中
reconstructedSentence += node.textContent;
} else if (node.classList.contains('word-slot')) {
// 如果是单词槽,获取其中的单词
const word = node.querySelector('.draggable-word');
if (word) {
reconstructedSentence += word.textContent;
}
} else if (node.classList.contains('punctuation')) {
// 如果是标点符号,添加到重构句子中
reconstructedSentence += node.textContent;
}
}
// 标准化句子(去除多余空格)进行比较
const normalizedOriginal = originalSentence.english.replace(/\s+/g, ' ').trim();
const normalizedReconstructed = reconstructedSentence.replace(/\s+/g, ' ').trim();
// 更新反馈信息和容器类名
if (normalizedOriginal === normalizedReconstructed) {
feedback.textContent = '恭喜!句子组装正确!';
feedback.className = 'feedback-message correct';
container.classList.add('correct');
container.classList.remove('incorrect');
// 朗读正确的句子
playCorrectSentence(originalSentence.english);
} else {
feedback.textContent = '句子组装不正确,请重试!';
feedback.className = 'feedback-message incorrect';
container.classList.add('incorrect');
container.classList.remove('correct');
}
}
// 朗读正确的句子
function playCorrectSentence(text) {
// 创建音频元素
const audio = new Audio();
// 获取语音设置
const voice = document.getElementById('voice').value;
const rate = document.getElementById('rate').value;
const pitch = document.getElementById('pitch').value;
// 构建 TTS 请求 URL
const encodedText = encodeURIComponent(text);
const cacheKey = btoa(`${text}_${voice}_${rate}_${pitch}`).replace(/[+/=]/g, '');
const url = `https://t.leftsite.cn/tts?t=${encodedText}&v=${voice}&r=${rate}&p=${pitch}&o=audio-24khz-48kbitrate-mono-mp3&cache=${cacheKey}`;
// 设置音频源并播放
audio.src = url;
audio.play();
}
// 关闭连词成句 Modal
async function hideWordArrangeModal() {
const modal = document.getElementById('wordArrangeModal');
// 如果处于全屏状态,退出全屏
if (document.fullscreenElement) {
try {
await document.exitFullscreen();
} catch (err) {
console.warn('Failed to exit fullscreen:', err);
}
}
// 清除相关状态
modal.classList.remove('fullscreen');
modal.style.display = 'none';
originalSentences = [];
}
// 自定义 contains 选择器,用于查找包含指定文本的元素
document.querySelectorAll = document.querySelectorAll || function (selector) {
if (selector.includes(':contains')) {
const text = selector.match(/'(.*?)'/)[1];
const elements = document.querySelectorAll(selector.split(':contains')[0]);
return Array.from(elements).filter(el => el.textContent.includes(text));
} else {
return document.querySelectorAll(selector);
}
};
// 添加有道API音频播放函数
function playYoudaoAudio(word) {
// 获取选择的发音类型
const pronunciationType = document.getElementById('pronunciationType').value;
// 构建有道API的音频URL
const audioUrl = `https://dict.youdao.com/dictvoice?audio=${encodeURIComponent(word)}&type=${pronunciationType}`;
// 创建音频元素
const audio = new Audio(audioUrl);
// 播放音频
audio.play().catch(error => {
console.error('播放音频失败:', error);
});
}
// 修改createDraggableWord函数,添加双击事件
function createDraggableWord(text) {
const word = document.createElement('div');
word.className = 'draggable-word word';
word.textContent = text;
word.draggable = true;
word.style.fontSize = document.getElementById('fontSize').value + 'px';
// 添加双击事件监听器
word.addEventListener('dblclick', function() {
playYoudaoAudio(this.textContent.toLowerCase());
});
return word;
}
</script>
</body>
</html>