This commit is contained in:
2025-08-28 12:59:00 +08:00
parent c14bbed2c2
commit 8d7f015d38
4 changed files with 573 additions and 633 deletions

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,65 @@
// 看板娘初始化
L2Dwidget.init({
"model": {
"jsonPath": "https://unpkg.com/live2d-widget-model-shizuku@1.0.5/assets/shizuku.model.json",
"scale": 1
},
"display": {
"position": "right",
"width": 150,
"height": 300,
"hOffset": 0,
"vOffset": -20
},
"mobile": {
"show": true,
"scale": 0.5
},
"react": {
"opacityDefault": 0.8,
"opacityOnHover": 0.2
}
});
// 模型选择器事件监听
document.getElementById('model-select').addEventListener('change', function() {
const selectedModel = this.value;
let modelPath;
switch(selectedModel) {
case 'shizuku':
modelPath = "https://unpkg.com/live2d-widget-model-shizuku@1.0.5/assets/shizuku.model.json";
break;
case 'koharu':
modelPath = "https://unpkg.com/live2d-widget-model-koharu@1.0.5/assets/koharu.model.json";
break;
case 'wanko':
modelPath = "https://unpkg.com/live2d-widget-model-wanko@1.0.5/assets/wanko.model.json";
break;
default:
modelPath = "https://unpkg.com/live2d-widget-model-shizuku@1.0.5/assets/shizuku.model.json";
}
// 重新初始化看板娘
L2Dwidget.init({
"model": {
"jsonPath": modelPath,
"scale": 1
},
"display": {
"position": "right",
"width": 150,
"height": 300,
"hOffset": 0,
"vOffset": -20
},
"mobile": {
"show": true,
"scale": 0.5
},
"react": {
"opacityDefault": 0.8,
"opacityOnHover": 0.2
}
});
});

View File

@@ -0,0 +1,504 @@
/* 全局样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
padding: 20px;
}
.container {
max-width: 900px;
margin: 0 auto;
background-color: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
padding: 30px;
}
header {
text-align: center;
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 2px solid #eaeaea;
}
h1 {
color: #2c3e50;
font-size: 28px;
margin-bottom: 15px;
}
.difficulty-indicator {
display: inline-block;
padding: 8px 16px;
border-radius: 20px;
font-weight: bold;
margin-bottom: 15px;
font-size: 16px;
}
.difficulty-medium {
background-color: #3498db;
color: white;
}
.difficulty-easy {
background-color: #2ecc71;
color: white;
}
.difficulty-hard {
background-color: #e74c3c;
color: white;
}
.quiz-info {
color: #7f8c8d;
font-size: 16px;
}
/* 题目样式 */
.question {
margin-bottom: 35px;
padding: 25px;
border-radius: 10px;
background-color: #f9f9f9;
border-left: 4px solid #3498db;
transition: all 0.3s ease;
}
.question:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.question-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.question-number {
font-weight: bold;
font-size: 18px;
color: #2c3e50;
}
.question-points {
background-color: #3498db;
color: white;
padding: 4px 10px;
border-radius: 15px;
font-size: 14px;
font-weight: bold;
}
.question-text {
font-size: 18px;
margin-bottom: 20px;
color: #34495e;
font-weight: 500;
}
.options {
margin-bottom: 20px;
}
.option {
margin-bottom: 12px;
padding: 12px 15px;
border-radius: 8px;
border: 2px solid #e0e0e0;
cursor: pointer;
transition: all 0.2s ease;
background-color: white;
display: flex;
align-items: center;
}
.option input[type="radio"] {
margin-right: 10px;
cursor: pointer;
flex-shrink: 0;
}
.option label {
cursor: pointer;
display: inline;
flex-grow: 1;
line-height: 1.4;
}
.option:hover {
border-color: #3498db;
background-color: #f0f8ff;
}
.question-explanation {
display: none;
margin-top: 15px;
padding: 15px;
background-color: #e8f4fd;
border-radius: 8px;
border-left: 4px solid #3498db;
color: #2c3e50;
}
.hidden {
display: none;
}
/* 导航按钮样式 */
.navigation-section {
text-align: center;
margin-top: 40px;
padding: 25px;
background-color: #f8f9fa;
border-radius: 10px;
display: none;
}
#next-btn {
background: linear-gradient(45deg, #667eea, #764ba2);
color: white;
border: none;
padding: 12px 30px;
border-radius: 30px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
position: relative;
overflow: hidden;
transition: all 0.3s ease;
}
#next-btn:before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: all 0.5s ease;
}
#next-btn:hover:before {
left: 100%;
}
#next-btn:hover {
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}
#next-btn:active {
transform: translateY(-1px);
box-shadow: 0 3px 10px rgba(102, 126, 234, 0.4);
}
.navigation-message {
font-size: 16px;
color: #333;
margin-bottom: 20px;
line-height: 1.6;
}
.submit-section {
text-align: center;
margin-top: 40px;
}
#submit-btn {
background-color: #2980b9;
color: white;
border: none;
padding: 12px 30px;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
transition: all 0.2s ease;
}
#submit-btn:hover {
background-color: #1e6fa5;
transform: translateY(-2px);
}
#result {
margin-top: 30px;
padding: 20px;
border-radius: 8px;
display: none;
}
.result-header {
font-size: 22px;
font-weight: bold;
margin-bottom: 15px;
color: #2c3e50;
}
.score {
font-size: 18px;
margin-bottom: 20px;
color: #333;
}
.correct-answers {
background-color: #e8f5e9;
color: #2e7d32;
padding: 10px 15px;
border-radius: 6px;
margin-bottom: 10px;
}
.incorrect-answers {
background-color: #ffebee;
color: #c62828;
padding: 10px 15px;
border-radius: 6px;
}
/* 学伴模型选择器样式 */
.model-selector {
position: fixed;
top: 40px;
left: 20px;
z-index: 1000;
padding: 10px;
background-color: rgba(255, 255, 255, 0.8);
border-radius: 5px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
gap: 15px;
}
select {
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background-color: white;
font-size: 14px;
}
/* 录音控制样式 */
.recording-controls {
position: static;
display: flex;
flex-direction: column;
gap: 10px;
margin: 0;
}
.record-button {
width: 70px;
height: 70px;
border-radius: 50%;
background-color: #dc3545;
border: none;
color: white;
font-size: 16px;
cursor: pointer;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
display: flex;
align-items: center;
justify-content: center;
opacity: 1 !important;
visibility: visible !important;
}
.record-button:hover {
background-color: #c82333;
}
.stop-button {
width: 70px;
height: 70px;
border-radius: 50%;
background-color: #6c757d;
border: none;
color: white;
font-size: 16px;
cursor: pointer;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
display: flex;
align-items: center;
justify-content: center;
display: none;
opacity: 1 !important;
visibility: visible !important;
}
/* 录音指示器样式 */
.recording-indicator {
position: fixed;
bottom: 20px;
left: 20px;
z-index: 1000;
padding: 10px 15px;
background-color: rgba(220, 53, 69, 0.9);
color: white;
border-radius: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
display: none;
align-items: center;
}
.recording-dot {
width: 10px;
height: 10px;
background-color: white;
border-radius: 50%;
margin-right: 8px;
animation: pulse 1.5s infinite;
}
/* 思考中动画样式 */
.thinking-indicator {
position: fixed;
bottom: 20px;
right: 120px;
z-index: 1000;
padding: 10px 15px;
background-color: rgba(0, 123, 255, 0.9);
color: white;
border-radius: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
display: none;
align-items: center;
}
.thinking-dots {
display: flex;
gap: 4px;
margin-right: 8px;
}
.thinking-dot {
width: 8px;
height: 8px;
background-color: white;
border-radius: 50%;
}
.thinking-dot:nth-child(1) {
animation: pulse 1.5s infinite 0s;
}
.thinking-dot:nth-child(2) {
animation: pulse 1.5s infinite 0.3s;
}
.thinking-dot:nth-child(3) {
animation: pulse 1.5s infinite 0.6s;
}
/* 结果显示容器样式 */
.result-container {
position: fixed;
bottom: 80px;
left: 20px;
z-index: 1000;
padding: 15px;
background-color: rgba(255, 255, 255, 0.95);
border-radius: 8px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
max-width: 400px;
width: 90%;
display: none;
flex-direction: column;
gap: 10px;
}
.result-header {
font-weight: bold;
color: #495057;
margin-bottom: 5px;
display: flex;
align-items: center;
gap: 5px;
}
.result-header.asr {
color: #007bff;
}
.result-header.feedback {
color: #28a745;
}
.result-text {
color: #333;
line-height: 1.5;
max-height: 200px;
overflow-y: auto;
padding-right: 5px;
word-break: break-all;
}
/* 音频播放器样式 */
.audio-player-container {
margin-top: 10px;
display: flex;
align-items: center;
gap: 10px;
}
.play-button {
background-color: #28a745;
color: white;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.play-button:hover {
background-color: #218838;
}
.audio-progress {
flex-grow: 1;
height: 6px;
background-color: #e9ecef;
border-radius: 3px;
overflow: hidden;
cursor: pointer;
}
.progress-bar {
height: 100%;
background-color: #28a745;
width: 0%;
}
.audio-time {
font-size: 12px;
color: #6c757d;
}
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}

View File

@@ -4,383 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>物理知识测验 - 万有引力定律</title> <title>物理知识测验 - 万有引力定律</title>
<style> <link rel="stylesheet" href="physics_quiz.css">
/* 全局样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
padding: 20px;
}
.container {
max-width: 900px;
margin: 0 auto;
background-color: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
padding: 30px;
}
header {
text-align: center;
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 2px solid #eaeaea;
}
h1 {
color: #2c3e50;
font-size: 28px;
margin-bottom: 15px;
}
.difficulty-indicator {
display: inline-block;
padding: 8px 16px;
border-radius: 20px;
font-weight: bold;
margin-bottom: 15px;
font-size: 16px;
}
.difficulty-medium {
background-color: #3498db;
color: white;
}
.difficulty-easy {
background-color: #2ecc71;
color: white;
}
.difficulty-hard {
background-color: #e74c3c;
color: white;
}
.quiz-info {
color: #7f8c8d;
font-size: 16px;
}
/* 题目样式 */
.question {
margin-bottom: 35px;
padding: 25px;
border-radius: 10px;
background-color: #f9f9f9;
border-left: 4px solid #3498db;
transition: all 0.3s ease;
}
.question:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.question-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.question-number {
font-weight: bold;
font-size: 18px;
color: #2c3e50;
}
.question-points {
background-color: #3498db;
color: white;
padding: 4px 10px;
border-radius: 15px;
font-size: 14px;
font-weight: bold;
}
.question-text {
font-size: 18px;
margin-bottom: 20px;
color: #34495e;
font-weight: 500;
}
.options {
margin-bottom: 20px;
}
.option {
margin-bottom: 12px;
padding: 12px 15px;
border-radius: 8px;
border: 2px solid #e0e0e0;
cursor: pointer;
transition: all 0.2s ease;
background-color: white;
display: flex;
align-items: center;
}
.option input[type="radio"] {
margin-right: 10px;
cursor: pointer;
flex-shrink: 0;
}
.option label {
cursor: pointer;
display: inline;
flex-grow: 1;
line-height: 1.4;
}
.option:hover {
border-color: #3498db;
background-color: #f0f8ff;
}
.option input[type="radio"] {
margin-right: 10px;
cursor: pointer;
}
.option label {
cursor: pointer;
display: block;
width: 100%;
}
.question-explanation {
display: none;
margin-top: 15px;
padding: 15px;
background-color: #e8f4fd;
border-radius: 8px;
border-left: 4px solid #3498db;
color: #2c3e50;
}
.hidden {
display: none;
}
/* 导航按钮样式 */
.navigation-section {
text-align: center;
margin-top: 40px;
padding: 25px;
background-color: #f8f9fa;
border-radius: 10px;
display: none;
}
#next-btn {
background: linear-gradient(45deg, #667eea, #764ba2);
color: white;
border: none;
padding: 12px 30px;
border-radius: 30px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
position: relative;
overflow: hidden;
transition: all 0.3s ease;
}
#next-btn:before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: all 0.5s ease;
}
#next-btn:hover:before {
left: 100%;
}
#next-btn:hover {
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}
#next-btn:active {
transform: translateY(-1px);
box-shadow: 0 3px 10px rgba(102, 126, 234, 0.4);
}
.navigation-message {
font-size: 16px;
color: #333;
margin-bottom: 20px;
line-height: 1.6;
}
.submit-section {
text-align: center;
margin-top: 40px;
}
#submit-btn {
background-color: #2980b9;
color: white;
border: none;
padding: 12px 30px;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
transition: all 0.2s ease;
}
#submit-btn:hover {
background-color: #1e6fa5;
transform: translateY(-2px);
}
#result {
margin-top: 30px;
padding: 20px;
border-radius: 8px;
display: none;
}
.result-header {
font-size: 22px;
font-weight: bold;
margin-bottom: 15px;
color: #2c3e50;
}
.score {
font-size: 18px;
margin-bottom: 20px;
color: #333;
}
.correct-answers {
background-color: #e8f5e9;
color: #2e7d32;
padding: 10px 15px;
border-radius: 6px;
margin-bottom: 10px;
}
.incorrect-answers {
background-color: #ffebee;
color: #c62828;
padding: 10px 15px;
border-radius: 6px;
}
/* 学伴模型选择器样式 */
.model-selector {
position: fixed; top: 40px; left: 20px; z-index: 1000;
padding: 10px; background-color: rgba(255, 255, 255, 0.8);
border-radius: 5px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
display: flex; flex-direction: column; gap: 15px;
}
select { padding: 8px 12px; border: 1px solid #ccc; border-radius: 4px; background-color: white; font-size: 14px; }
/* 录音控制样式 */
.recording-controls {
position: static; display: flex; flex-direction: column; gap: 10px; margin: 0;
}
.record-button {
width: 70px; height: 70px; border-radius: 50%;
background-color: #dc3545; border: none; color: white;
font-size: 16px; cursor: pointer; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
display: flex; align-items: center; justify-content: center;
opacity: 1 !important; visibility: visible !important;
}
.record-button:hover { background-color: #c82333; }
.stop-button {
width: 70px; height: 70px; border-radius: 50%;
background-color: #6c757d; border: none; color: white;
font-size: 16px; cursor: pointer; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
display: flex; align-items: center; justify-content: center; display: none;
opacity: 1 !important; visibility: visible !important;
}
/* 录音指示器样式 */
.recording-indicator {
position: fixed; bottom: 20px; left: 20px; z-index: 1000;
padding: 10px 15px; background-color: rgba(220, 53, 69, 0.9);
color: white; border-radius: 20px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
display: none; align-items: center;
}
.recording-dot {
width: 10px; height: 10px; background-color: white;
border-radius: 50%; margin-right: 8px; animation: pulse 1.5s infinite;
}
/* 思考中动画样式 */
.thinking-indicator {
position: fixed; bottom: 20px; right: 120px; z-index: 1000;
padding: 10px 15px; background-color: rgba(0, 123, 255, 0.9);
color: white; border-radius: 20px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
display: none; align-items: center;
}
.thinking-dots { display: flex; gap: 4px; margin-right: 8px; }
.thinking-dot {
width: 8px; height: 8px; background-color: white; border-radius: 50%;
}
.thinking-dot:nth-child(1) { animation: pulse 1.5s infinite 0s; }
.thinking-dot:nth-child(2) { animation: pulse 1.5s infinite 0.3s; }
.thinking-dot:nth-child(3) { animation: pulse 1.5s infinite 0.6s; }
/* 结果显示容器样式 */
.result-container {
position: fixed; bottom: 80px; left: 20px; z-index: 1000;
padding: 15px; background-color: rgba(255, 255, 255, 0.95);
border-radius: 8px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
max-width: 400px; width: 90%;
display: none; flex-direction: column; gap: 10px;
}
.result-header {
font-weight: bold; color: #495057; margin-bottom: 5px;
display: flex; align-items: center; gap: 5px;
}
.result-header.asr { color: #007bff; }
.result-header.feedback { color: #28a745; }
.result-text {
color: #333; line-height: 1.5; max-height: 200px; overflow-y: auto;
padding-right: 5px; word-break: break-all;
}
/* 音频播放器样式 */
.audio-player-container {
margin-top: 10px; display: flex; align-items: center; gap: 10px;
}
.play-button {
background-color: #28a745; color: white; border: none;
border-radius: 50%; width: 40px; height: 40px;
cursor: pointer; display: flex; align-items: center; justify-content: center;
}
.play-button:hover { background-color: #218838; }
.audio-progress {
flex-grow: 1; height: 6px; background-color: #e9ecef; border-radius: 3px;
overflow: hidden; cursor: pointer;
}
.progress-bar {
height: 100%; background-color: #28a745; width: 0%;
}
.audio-time { font-size: 12px; color: #6c757d; }
@keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.5; } 100% { opacity: 1; } }
</style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
@@ -482,260 +106,9 @@
<div> <div>
有权限问题的话,请点击<a href="https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/11%E3%80%81%E5%AD%A6%E4%BC%B4Chrome%E5%BD%95%E9%9F%B3%E9%85%8D%E7%BD%AE%E4%BF%AE%E6%94%B9.pdf">这里</a> 有权限问题的话,请点击<a href="https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/11%E3%80%81%E5%AD%A6%E4%BC%B4Chrome%E5%BD%95%E9%9F%B3%E9%85%8D%E7%BD%AE%E4%BF%AE%E6%94%B9.pdf">这里</a>
</div> </div>
<!-- 引入看板娘相关脚本 -->
<script src="https://l2dwidget.js.org/lib/L2Dwidget.min.js"></script> <script src="https://l2dwidget.js.org/lib/L2Dwidget.min.js"></script>
<script> <script src="live2d_widget.js"></script>
// 录音相关变量
let mediaRecorder; let audioChunks = []; let isRecording = false;
// 音频播放相关变量
let audioElement = null; let isPlaying = false;
// 开始录音
function startRecording() {
if (isRecording) return;
console.log("尝试开始录音");
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
mediaRecorder = new MediaRecorder(stream);
audioChunks = [];
mediaRecorder.ondataavailable = event => {
if (event.data.size > 0) audioChunks.push(event.data);
};
mediaRecorder.onstop = () => {
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
console.log("录音完成,音频数据大小:", audioBlob.size);
uploadAudioToServer(audioBlob);
};
mediaRecorder.start();
isRecording = true;
document.getElementById('recordingIndicator').style.display = 'flex';
document.getElementById('startRecordBtn').style.display = 'none';
document.getElementById('stopRecordBtn').style.display = 'flex';
console.log("开始录音成功");
// 设置最长录音时间为60秒
setTimeout(stopRecording, 60000);
})
.catch(error => {
console.error("获取麦克风权限失败:", error);
alert("请授权麦克风权限以使用录音功能");
});
}
// 停止录音
function stopRecording() {
if (!isRecording || !mediaRecorder) return;
mediaRecorder.stop();
isRecording = false;
document.getElementById('recordingIndicator').style.display = 'none';
document.getElementById('startRecordBtn').style.display = 'flex';
document.getElementById('stopRecordBtn').style.display = 'none';
console.log("停止录音");
if (mediaRecorder.stream) {
mediaRecorder.stream.getTracks().forEach(track => track.stop());
}
}
// 上传音频到服务器
function uploadAudioToServer(audioBlob) {
console.log("开始上传音频到服务器");
document.getElementById('thinkingIndicator').style.display = 'flex';
const formData = new FormData();
formData.append('file', audioBlob, 'recording.wav');
fetch('/api/xueban/upload-audio', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) throw new Error('服务器响应错误');
return response.json();
})
.then(data => {
console.log("处理结果:", data);
document.getElementById('thinkingIndicator').style.display = 'none';
if (data.success) {
showResults(data.data);
} else {
alert('音频处理失败: ' + data.message);
}
})
.catch(error => {
console.error("上传音频失败:", error);
document.getElementById('thinkingIndicator').style.display = 'none';
alert('上传音频失败: ' + error.message);
});
}
// 显示ASR识别结果和反馈
function showResults(data) {
const resultContainer = document.getElementById('resultContainer');
resultContainer.style.display = 'flex';
document.getElementById('asrResultText').textContent = data.asr_text || '未识别到内容';
document.getElementById('feedbackResultText').textContent = data.feedback_text || '无反馈内容';
if (data.audio_url) {
if (audioElement) audioElement.pause();
audioElement = new Audio(data.audio_url);
audioElement.onloadedmetadata = function() {
updateAudioTimeDisplay();
try { audioElement.play(); isPlaying = true; updatePlayButton(); }
catch (e) { console.error("自动播放失败:", e); }
document.getElementById('playAudioBtn').style.display = 'flex';
};
audioElement.ontimeupdate = function() { updateAudioProgress(); updateAudioTimeDisplay(); };
audioElement.onended = function() { isPlaying = false; updatePlayButton(); };
document.getElementById('playAudioBtn').onclick = togglePlayAudio;
document.getElementById('audioProgress').onclick = function(e) {
if (!audioElement) return;
const rect = this.getBoundingClientRect();
const clickPosition = (e.clientX - rect.left) / rect.width;
audioElement.currentTime = clickPosition * audioElement.duration;
};
}
}
// 音频播放控制函数
function togglePlayAudio() { if (!audioElement) return; isPlaying ? audioElement.pause() : audioElement.play(); isPlaying = !isPlaying; updatePlayButton(); }
function updatePlayButton() {
const btn = document.getElementById('playAudioBtn');
btn.innerHTML = isPlaying ? `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 19H10V5H6V19ZM14 19H18V5H14V19Z" fill="white"/>
</svg>` : `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 5V19L19 12L8 5Z" fill="white"/>
</svg>`;
}
function updateAudioProgress() { if (audioElement) document.getElementById('progressBar').style.width = `${(audioElement.currentTime/audioElement.duration)*100}%`; }
function updateAudioTimeDisplay() {
if (!audioElement) return;
const format = s => `${Math.floor(s/60).toString().padStart(2,'0')}:${Math.floor(s%60).toString().padStart(2,'0')}`;
document.getElementById('audioTime').textContent = `${format(audioElement.currentTime)} / ${format(audioElement.duration)}`;
}
// 模型配置映射
const modelConfig = {
shizuku: {
jsonPath: "https://unpkg.com/live2d-widget-model-shizuku@1.0.5/assets/shizuku.model.json"
},
koharu: {
jsonPath: "https://unpkg.com/live2d-widget-model-koharu@1.0.5/assets/koharu.model.json"
},
wanko: {
jsonPath: "https://unpkg.com/live2d-widget-model-wanko@1.0.5/assets/wanko.model.json"
}
};
let currentL2DWidget = null;
let modelElement = null;
// 获取URL参数
function getUrlParam(name) {
const reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)');
const r = window.location.search.substr(1).match(reg);
return r ? unescape(r[2]) : null;
}
// 初始化看板娘
function initL2DWidget(modelName) {
// 如果没有传入modelName则从URL参数获取模型名称默认使用shizuku
if (!modelName) {
modelName = getUrlParam('model') || 'shizuku';
}
console.log("切换模型: ", modelName);
const config = modelConfig[modelName];
if (!config) {
console.error("模型配置不存在: ", modelName);
return;
}
console.log("模型JSON路径: ", config.jsonPath);
// 彻底清理现有实例
if (currentL2DWidget) {
try {
currentL2DWidget.destroy();
console.log("旧模型实例已销毁");
} catch (e) {
console.error("销毁实例失败: ", e);
}
// 修复强制移除所有canvas元素
const oldCanvases = document.querySelectorAll('canvas#l2dcanvas');
oldCanvases.forEach(canvas => canvas.remove());
currentL2DWidget = null;
modelElement = null;
}
// 创建新实例
try {
currentL2DWidget = L2Dwidget.init({
model: {
jsonPath: config.jsonPath,
scale: 1
},
display: {
position: "right",
width: 150,
height: 300,
hOffset: 0,
vOffset: -20
},
mobile: {
show: true,
scale: 0.5
},
react: {
opacityDefault: 0.7,
opacityOnHover: 0.8
}
});
// 保存模型DOM元素引用
// 修复使用setTimeout确保DOM更新完成后再获取元素
setTimeout(() => {
modelElement = document.querySelector('canvas#l2dcanvas');
if (modelElement) {
console.log("新模型DOM元素已获取");
} else {
console.warn("未找到新模型canvas元素");
}
console.log("新模型初始化成功");
}, 100);
} catch (e) {
console.error("模型初始化失败: ", e);
}
}
// 监听模型选择变化
const modelSelect = document.getElementById('model-select');
if (modelSelect) {
// 设置当前选中项
const currentModel = getUrlParam('model') || 'shizuku';
modelSelect.value = currentModel;
modelSelect.addEventListener('change', function() {
console.log("选择模型: ", this.value);
// 通过URL参数刷新页面实现模型切换
window.location.search = '?model=' + this.value;
});
console.log("模型选择监听器已绑定");
} else {
console.error("未找到模型选择器元素");
}
// 初始加载默认模型
document.addEventListener('DOMContentLoaded', function() {
initL2DWidget();
// 添加录音按钮事件绑定
document.getElementById('startRecordBtn').addEventListener('click', startRecording);
document.getElementById('stopRecordBtn').addEventListener('click', stopRecording);
});
</script>
</body> </body>
</html> </html>