335 lines
9.7 KiB
JavaScript
335 lines
9.7 KiB
JavaScript
/**
|
||
* Dashboard页面 JavaScript - 增强动效版
|
||
*/
|
||
|
||
// DOM加载完成后初始化
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
initializeDashboard();
|
||
loadVoiceStatus();
|
||
bindEvents();
|
||
});
|
||
|
||
/**
|
||
* 初始化Dashboard - 增强版
|
||
*/
|
||
function initializeDashboard() {
|
||
// 添加页面加载动画类
|
||
document.body.classList.add('page-loading');
|
||
|
||
// 为所有主要元素添加渐入动画类
|
||
setTimeout(() => {
|
||
document.querySelectorAll('.welcome-card-kid, .scenario-card-kid, .voice-clone-card, .quick-action-card-kid, .achievement-card').forEach((el, index) => {
|
||
el.classList.add('fade-in-up');
|
||
el.style.animationDelay = `${index * 0.1}s`;
|
||
});
|
||
}, 100);
|
||
|
||
// 移除加载状态
|
||
setTimeout(() => {
|
||
document.body.classList.remove('page-loading');
|
||
}, 2000);
|
||
|
||
// 添加页面加载动画
|
||
animateCards();
|
||
|
||
// 初始化成就数据
|
||
animateAchievements();
|
||
}
|
||
|
||
/**
|
||
* 绑定事件
|
||
*/
|
||
function bindEvents() {
|
||
// 场景卡片点击事件
|
||
document.querySelectorAll('[data-scenario]').forEach(btn => {
|
||
btn.addEventListener('click', function() {
|
||
const scenarioId = this.getAttribute('data-scenario');
|
||
handleScenarioClick(scenarioId);
|
||
});
|
||
});
|
||
|
||
// 快速测试按钮
|
||
const quickTestBtn = document.getElementById('quick-test-btn');
|
||
if (quickTestBtn) {
|
||
quickTestBtn.addEventListener('click', handleQuickTest);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 卡片加载动画
|
||
*/
|
||
function animateCards() {
|
||
const cards = document.querySelectorAll('.scenario-card-kid, .quick-action-card-kid, .achievement-card');
|
||
|
||
cards.forEach((card, index) => {
|
||
card.style.opacity = '0';
|
||
card.style.transform = 'translateY(30px)';
|
||
|
||
setTimeout(() => {
|
||
card.style.transition = 'all 0.5s ease';
|
||
card.style.opacity = '1';
|
||
card.style.transform = 'translateY(0)';
|
||
}, index * 100);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 成就数据动画
|
||
*/
|
||
function animateAchievements() {
|
||
const achievements = [
|
||
{ selector: '.time-card .achievement-number', target: 0, suffix: '' },
|
||
{ selector: '.conversation-card .achievement-number', target: 0, suffix: '' },
|
||
{ selector: '.star-card .achievement-number', target: 0, suffix: '' },
|
||
{ selector: '.medal-card .achievement-number', target: 0, suffix: '' }
|
||
];
|
||
|
||
achievements.forEach((achievement, index) => {
|
||
setTimeout(() => {
|
||
animateNumber(achievement.selector, 0, achievement.target, 1000, achievement.suffix);
|
||
}, index * 200);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 数字动画
|
||
*/
|
||
function animateNumber(selector, start, end, duration, suffix = '') {
|
||
const element = document.querySelector(selector);
|
||
if (!element) return;
|
||
|
||
const range = end - start;
|
||
const minTimer = 50;
|
||
const stepTime = Math.abs(Math.floor(duration / range));
|
||
const timer = stepTime < minTimer ? minTimer : stepTime;
|
||
|
||
const startTime = new Date().getTime();
|
||
const endTime = startTime + duration;
|
||
|
||
function run() {
|
||
const now = new Date().getTime();
|
||
const remaining = Math.max((endTime - now) / duration, 0);
|
||
const value = Math.round(end - (remaining * range));
|
||
|
||
element.textContent = value + suffix;
|
||
|
||
if (value === end) {
|
||
return;
|
||
}
|
||
|
||
setTimeout(run, timer);
|
||
}
|
||
|
||
run();
|
||
}
|
||
|
||
/**
|
||
* 加载语音状态
|
||
*/
|
||
async function loadVoiceStatus() {
|
||
try {
|
||
const response = await fetch('/voice-clone/api/voice-clone/status');
|
||
const result = await response.json();
|
||
|
||
updateVoiceStatusDisplay(result);
|
||
|
||
} catch (error) {
|
||
console.error('加载语音状态失败:', error);
|
||
updateVoiceStatusDisplay({
|
||
success: false,
|
||
has_sample: false,
|
||
message: '无法加载语音状态'
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新语音状态显示
|
||
*/
|
||
function updateVoiceStatusDisplay(result) {
|
||
const display = document.getElementById('voice-status-display');
|
||
const quickTestBtn = document.getElementById('quick-test-btn');
|
||
|
||
if (result.has_sample && result.success) {
|
||
display.innerHTML = `
|
||
<h4 class="fw-bold mb-2">
|
||
<i class="fas fa-check-circle text-success me-2"></i>
|
||
你的专属声音已就绪!
|
||
</h4>
|
||
<p class="mb-1">AI已经学会了你的声音:「${result.recognized_text}」</p>
|
||
<small class="opacity-75">录制时间:${result.upload_time}</small>
|
||
`;
|
||
|
||
if (quickTestBtn) {
|
||
quickTestBtn.disabled = false;
|
||
}
|
||
|
||
// 激活语音波浪动画
|
||
document.querySelectorAll('.voice-wave').forEach((wave, index) => {
|
||
wave.style.animationDelay = `${index * 0.2}s`;
|
||
wave.style.animationPlayState = 'running';
|
||
});
|
||
|
||
} else {
|
||
display.innerHTML = `
|
||
<h4 class="fw-bold mb-2">
|
||
<i class="fas fa-microphone-slash text-warning me-2"></i>
|
||
还没有录制语音样本
|
||
</h4>
|
||
<p class="mb-0">录制你的专属声音,让AI学会模仿你说话!</p>
|
||
`;
|
||
|
||
if (quickTestBtn) {
|
||
quickTestBtn.disabled = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理场景点击
|
||
*/
|
||
function handleScenarioClick(scenarioId) {
|
||
// 显示动画效果
|
||
showComingSoonAnimation();
|
||
}
|
||
|
||
/**
|
||
* 快速测试语音克隆
|
||
*/
|
||
async function handleQuickTest() {
|
||
const btn = document.getElementById('quick-test-btn');
|
||
const originalText = btn.innerHTML;
|
||
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>测试中...';
|
||
|
||
try {
|
||
const response = await fetch('/voice-clone/api/voice-clone/generate', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
text: '你好,这是我的专属声音测试!'
|
||
})
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
showSuccess('快速测试成功!播放你的专属声音');
|
||
|
||
// 播放生成的音频
|
||
const audio = new Audio();
|
||
const filename = result.audio_url.split('/').pop();
|
||
audio.src = `/voice-test/download-audio/${filename}`;
|
||
audio.play().catch(e => console.error('播放失败:', e));
|
||
|
||
} else {
|
||
showError(result.message || '测试失败');
|
||
}
|
||
|
||
} catch (error) {
|
||
showError('测试失败,请检查网络连接');
|
||
console.error('快速测试失败:', error);
|
||
} finally {
|
||
btn.disabled = false;
|
||
btn.innerHTML = originalText;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 显示即将上线动画
|
||
*/
|
||
function showComingSoonAnimation() {
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal fade';
|
||
modal.innerHTML = `
|
||
<div class="modal-dialog modal-dialog-centered">
|
||
<div class="modal-content border-0" style="border-radius: 20px;">
|
||
<div class="modal-body text-center py-5">
|
||
<div class="mb-4">
|
||
<i class="fas fa-rocket" style="font-size: 4rem; color: #4ECDC4; animation: bounce 1s ease-in-out infinite;"></i>
|
||
</div>
|
||
<h3 class="fw-bold text-primary mb-3">敬请期待!</h3>
|
||
<p class="text-muted mb-4">这个超棒的功能正在努力开发中<br>很快就能和小朋友们见面啦!</p>
|
||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" style="border-radius: 25px; padding: 0.8rem 2rem;">
|
||
<i class="fas fa-heart me-2"></i>好期待哦
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(modal);
|
||
|
||
const bsModal = new bootstrap.Modal(modal);
|
||
bsModal.show();
|
||
|
||
// 自动清理
|
||
modal.addEventListener('hidden.bs.modal', () => {
|
||
document.body.removeChild(modal);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 显示成功消息
|
||
*/
|
||
function showSuccess(message) {
|
||
showToast(message, 'success');
|
||
}
|
||
|
||
/**
|
||
* 显示错误消息
|
||
*/
|
||
function showError(message) {
|
||
showToast(message, 'danger');
|
||
}
|
||
|
||
/**
|
||
* 显示Toast消息
|
||
*/
|
||
function showToast(message, type = 'info') {
|
||
const colors = {
|
||
'success': 'bg-success',
|
||
'danger': 'bg-danger',
|
||
'warning': 'bg-warning',
|
||
'info': 'bg-info'
|
||
};
|
||
|
||
const toast = document.createElement('div');
|
||
toast.className = `toast align-items-center text-white ${colors[type]} border-0 position-fixed top-0 end-0 m-3`;
|
||
toast.style.zIndex = '9999';
|
||
toast.innerHTML = `
|
||
<div class="d-flex">
|
||
<div class="toast-body">
|
||
<i class="fas fa-check-circle me-2"></i>${message}
|
||
</div>
|
||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(toast);
|
||
const bsToast = new bootstrap.Toast(toast);
|
||
bsToast.show();
|
||
|
||
setTimeout(() => {
|
||
if (toast.parentNode) {
|
||
toast.parentNode.removeChild(toast);
|
||
}
|
||
}, 3000);
|
||
}
|
||
|
||
// 添加鼠标交互效果
|
||
document.addEventListener('mousemove', function(e) {
|
||
// 鼠标跟随效果(轻微)
|
||
const shapes = document.querySelectorAll('.shape');
|
||
shapes.forEach((shape, index) => {
|
||
const speed = (index + 1) * 0.01;
|
||
const x = e.clientX * speed;
|
||
const y = e.clientY * speed;
|
||
|
||
shape.style.transform += ` translate(${x}px, ${y}px)`;
|
||
});
|
||
});
|