FALMOVIE / public /script.js
Logankunfall's picture
Upload 8 files
4ccfabc verified
// 全局变量
let currentTheme = 'light';
let isGenerating = false;
let currentApiKey = null;
// 初始化
document.addEventListener('DOMContentLoaded', function() {
console.log('页面加载完成,开始初始化...');
try {
initializeTheme();
initializeEventListeners();
initializeRangeSliders();
// 先加载本地缓存的 API 密钥,避免未传 key 导致 401
loadLocalApiKey();
// 再检查后端密钥状态(环境变量/服务端存储)
checkApiKeyStatus();
loadVideoLibrary();
console.log('初始化完成');
} catch (error) {
console.error('初始化失败:', error);
}
});
// 主题切换功能
function initializeTheme() {
const savedTheme = localStorage.getItem('theme') || 'light';
setTheme(savedTheme);
}
function setTheme(theme) {
currentTheme = theme;
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
const themeToggle = document.getElementById('themeToggle');
if (themeToggle) {
const icon = themeToggle.querySelector('i');
if (icon) {
if (theme === 'dark') {
icon.className = 'fas fa-sun';
themeToggle.title = '切换到浅色模式';
} else {
icon.className = 'fas fa-moon';
themeToggle.title = '切换到深色模式';
}
}
}
}
function toggleTheme() {
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
}
// 从本地缓存读取 API 密钥并启用(不改后端)
function loadLocalApiKey() {
try {
const cached = localStorage.getItem('fal_api_key');
if (cached && cached.length >= 10) {
currentApiKey = cached;
const apiKeyStatus = document.getElementById('apiKeyStatus');
const apiKeyButton = document.getElementById('apiKeyButton');
const modalApiKeyStatus = document.getElementById('modalApiKeyStatus');
if (apiKeyStatus) apiKeyStatus.textContent = '本地缓存';
if (apiKeyButton) apiKeyButton.classList.add('configured');
if (modalApiKeyStatus) modalApiKeyStatus.textContent = '已配置(来源:本地缓存)';
}
} catch (e) {
console.warn('读取本地缓存的 API key 失败:', e);
}
}
// API Key 管理功能
async function checkApiKeyStatus() {
try {
const response = await fetch('/api/check-key');
const data = await response.json();
updateApiKeyStatus(data);
} catch (error) {
console.error('检查 API key 状态失败:', error);
updateApiKeyStatus({ hasStoredKey: false, hasEnvKey: false, keySource: 'none' });
}
}
function updateApiKeyStatus(status) {
const { hasStoredKey, hasEnvKey, keySource } = status;
const apiKeyStatus = document.getElementById('apiKeyStatus');
const apiKeyButton = document.getElementById('apiKeyButton');
const modalApiKeyStatus = document.getElementById('modalApiKeyStatus');
if (apiKeyStatus) {
if (keySource === 'environment') {
apiKeyStatus.textContent = '环境变量';
if (apiKeyButton) apiKeyButton.classList.add('configured');
if (modalApiKeyStatus) modalApiKeyStatus.textContent = '已配置(来源:环境变量)';
} else if (keySource === 'stored') {
apiKeyStatus.textContent = '已保存';
if (apiKeyButton) apiKeyButton.classList.add('configured');
if (modalApiKeyStatus) modalApiKeyStatus.textContent = '已配置(来源:本地存储)';
} else {
apiKeyStatus.textContent = '未配置';
if (apiKeyButton) apiKeyButton.classList.remove('configured');
if (modalApiKeyStatus) modalApiKeyStatus.textContent = '未配置 - 请输入 API 密钥';
}
}
}
// 事件监听器初始化
function initializeEventListeners() {
console.log('初始化事件监听器...');
// 主题切换
const themeToggle = document.getElementById('themeToggle');
if (themeToggle) {
themeToggle.addEventListener('click', toggleTheme);
console.log('主题切换按钮已绑定');
}
// API Key 管理
const apiKeyButton = document.getElementById('apiKeyButton');
const apiKeyModal = document.getElementById('apiKeyModal');
const closeModal = document.getElementById('closeModal');
if (apiKeyButton && apiKeyModal) {
apiKeyButton.addEventListener('click', () => {
apiKeyModal.style.display = 'flex';
checkApiKeyStatus();
});
console.log('API Key 按钮已绑定');
}
if (closeModal && apiKeyModal) {
closeModal.addEventListener('click', () => {
apiKeyModal.style.display = 'none';
});
// 点击模态框背景关闭
apiKeyModal.addEventListener('click', (e) => {
if (e.target === apiKeyModal) {
apiKeyModal.style.display = 'none';
}
});
console.log('模态框关闭事件已绑定');
}
// API Key 相关按钮
const toggleApiKeyVisibility = document.getElementById('toggleApiKeyVisibility');
const saveApiKeyButton = document.getElementById('saveApiKeyButton');
const testApiKeyButton = document.getElementById('testApiKeyButton');
if (toggleApiKeyVisibility) {
toggleApiKeyVisibility.addEventListener('click', toggleApiKeyVisibilityFunc);
}
if (saveApiKeyButton) {
saveApiKeyButton.addEventListener('click', saveApiKey);
}
if (testApiKeyButton) {
testApiKeyButton.addEventListener('click', testApiKey);
}
// ESC 键关闭模态框
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && apiKeyModal && apiKeyModal.style.display === 'flex') {
apiKeyModal.style.display = 'none';
}
});
// 标签页切换
const tabButtons = document.querySelectorAll('.tab-button');
tabButtons.forEach(button => {
button.addEventListener('click', () => switchTab(button.dataset.tab));
});
console.log(`${tabButtons.length} 个标签按钮已绑定`);
// 图片上传相关
const imageUploadArea = document.getElementById('imageUploadArea');
const imageFile = document.getElementById('imageFile');
const removeImage = document.getElementById('removeImage');
if (imageUploadArea && imageFile) {
imageUploadArea.addEventListener('click', () => imageFile.click());
imageUploadArea.addEventListener('dragover', handleDragOver);
imageUploadArea.addEventListener('drop', handleDrop);
imageUploadArea.addEventListener('dragleave', handleDragLeave);
imageFile.addEventListener('change', handleImageSelect);
console.log('图片上传事件已绑定');
}
if (removeImage) {
removeImage.addEventListener('click', clearImagePreview);
}
// 表单提交
const imageToVideoForm = document.getElementById('imageToVideoForm');
const textToVideoForm = document.getElementById('textToVideoForm');
if (imageToVideoForm) {
imageToVideoForm.addEventListener('submit', handleImageToVideoSubmit);
console.log('图片转视频表单已绑定');
}
if (textToVideoForm) {
textToVideoForm.addEventListener('submit', handleTextToVideoSubmit);
console.log('文本转视频表单已绑定');
}
// 结果清除
const clearResult = document.getElementById('clearResult');
if (clearResult) {
clearResult.addEventListener('click', clearResults);
}
// 视频库相关
const refreshLibrary = document.getElementById('refreshLibrary');
if (refreshLibrary) {
refreshLibrary.addEventListener('click', loadVideoLibrary);
}
// 模型选择(文本转视频)
const t2vModelSelect = document.getElementById('t2vModel');
if (t2vModelSelect) {
t2vModelSelect.addEventListener('change', updateModelSettingsVisibility);
// 初始化时根据默认选择显示对应参数
updateModelSettingsVisibility();
console.log('模型选择切换事件已绑定');
}
// 模型选择(图片转视频)
const i2vModelSelect = document.getElementById('i2vModel');
if (i2vModelSelect) {
i2vModelSelect.addEventListener('change', updateI2VModelSettingsVisibility);
// 初始化时根据默认选择显示对应参数
updateI2VModelSettingsVisibility();
console.log('图片转视频模型选择切换事件已绑定');
}
console.log('事件监听器初始化完成');
}
// 范围滑块初始化
function initializeRangeSliders() {
const ranges = document.querySelectorAll('.form-range');
ranges.forEach(range => {
const valueSpan = document.getElementById(range.id + 'Value');
if (valueSpan) {
valueSpan.textContent = range.value;
range.addEventListener('input', () => {
valueSpan.textContent = range.value;
});
}
});
console.log(`${ranges.length} 个滑块已初始化`);
}
// 根据模型选择显示/隐藏对应高级设置
function updateModelSettingsVisibility() {
const model = document.getElementById('t2vModel')?.value || 'seedance-pro-fast';
const wanSettings = document.getElementById('wanSettings');
const seedSettings = document.getElementById('seedanceSettings');
if (wanSettings && seedSettings) {
if (model === 'wan-v2.2-a14b') {
wanSettings.style.display = 'block';
seedSettings.style.display = 'none';
} else {
wanSettings.style.display = 'none';
seedSettings.style.display = 'block';
}
}
}
// 根据图片转视频模型选择显示/隐藏对应高级设置
function updateI2VModelSettingsVisibility() {
const model = document.getElementById('i2vModel')?.value || 'seedance-pro-fast';
const wanSettings = document.getElementById('i2vWanSettings');
const seedSettings = document.getElementById('i2vSeedanceSettings');
if (wanSettings && seedSettings) {
if (model === 'wan-v2.2-a14b') {
wanSettings.style.display = 'block';
seedSettings.style.display = 'none';
} else {
wanSettings.style.display = 'none';
seedSettings.style.display = 'block';
}
}
}
// API Key 相关函数
function toggleApiKeyVisibilityFunc() {
const apiKeyInput = document.getElementById('apiKeyInput');
const toggleButton = document.getElementById('toggleApiKeyVisibility');
if (apiKeyInput && toggleButton) {
const icon = toggleButton.querySelector('i');
if (apiKeyInput.type === 'password') {
apiKeyInput.type = 'text';
if (icon) icon.className = 'fas fa-eye-slash';
} else {
apiKeyInput.type = 'password';
if (icon) icon.className = 'fas fa-eye';
}
}
}
async function saveApiKey() {
const apiKeyInput = document.getElementById('apiKeyInput');
const saveApiKeyCheckbox = document.getElementById('saveApiKey');
const saveApiKeyButton = document.getElementById('saveApiKeyButton');
if (!apiKeyInput) return;
const apiKey = apiKeyInput.value.trim();
const shouldSave = saveApiKeyCheckbox ? saveApiKeyCheckbox.checked : true;
if (!apiKey) {
showNotification('请输入 API 密钥', 'error');
return;
}
// 基本检查
if (apiKey.length < 10) {
showNotification('API 密钥长度不足', 'error');
return;
}
// 显示保存状态
if (saveApiKeyButton) {
saveApiKeyButton.disabled = true;
saveApiKeyButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 保存中...';
}
if (shouldSave) {
try {
console.log('正在保存 API 密钥到服务器...');
const response = await fetch('/api/save-key', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ apiKey })
});
const result = await response.json();
console.log('保存结果:', result);
if (result.success) {
showNotification('API 密钥保存成功', 'success');
// 同步前端使用,避免未传 key 导致 401
currentApiKey = apiKey;
try { localStorage.setItem('fal_api_key', apiKey); } catch (e) {}
checkApiKeyStatus();
const apiKeyModal = document.getElementById('apiKeyModal');
if (apiKeyModal) apiKeyModal.style.display = 'none';
} else {
showNotification(result.error || '保存失败', 'error');
console.error('保存失败:', result);
}
} catch (error) {
console.error('保存 API key 失败:', error);
showNotification('保存失败,请检查网络连接', 'error');
}
} else {
// 临时设置
currentApiKey = apiKey;
showNotification('API 密钥已设置(临时)', 'success');
const apiKeyModal = document.getElementById('apiKeyModal');
if (apiKeyModal) apiKeyModal.style.display = 'none';
const apiKeyStatus = document.getElementById('apiKeyStatus');
const apiKeyButton = document.getElementById('apiKeyButton');
if (apiKeyStatus) apiKeyStatus.textContent = '临时设置';
if (apiKeyButton) apiKeyButton.classList.add('configured');
}
// 恢复按钮状态
if (saveApiKeyButton) {
saveApiKeyButton.disabled = false;
saveApiKeyButton.innerHTML = '<i class="fas fa-save"></i> 保存设置';
}
}
async function testApiKey() {
const testApiKeyButton = document.getElementById('testApiKeyButton');
const apiKeyInput = document.getElementById('apiKeyInput');
if (!testApiKeyButton) return;
const apiKey = apiKeyInput ? apiKeyInput.value.trim() : null;
testApiKeyButton.disabled = true;
testApiKeyButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 连接中...';
try {
const response = await fetch('/api/test-key', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ apiKey })
});
const result = await response.json();
if (result.success) {
showNotification(`✅ ${result.message}`, 'success');
if (result.note) {
setTimeout(() => {
showNotification(result.note, 'info');
}, 1500);
}
} else {
showNotification(result.error || 'API 密钥格式验证失败', 'error');
// 显示具体的格式问题
if (result.tips && result.tips.length > 0) {
console.error('格式问题:', result.tips);
setTimeout(() => {
result.tips.forEach((tip, index) => {
setTimeout(() => {
showNotification(tip, 'warning');
}, index * 1000);
});
}, 1000);
}
}
} catch (error) {
console.error('测试 API key 失败:', error);
showNotification('网络连接失败,请重试', 'error');
} finally {
testApiKeyButton.disabled = false;
testApiKeyButton.innerHTML = '<i class="fas fa-plug"></i> 测试连接';
}
}
// 标签页切换
function switchTab(tabId) {
const tabButtons = document.querySelectorAll('.tab-button');
const tabContents = document.querySelectorAll('.tab-content');
tabButtons.forEach(btn => {
btn.classList.toggle('active', btn.dataset.tab === tabId);
});
tabContents.forEach(content => {
content.classList.toggle('active', content.id === tabId);
});
clearResults();
}
// 图片处理函数
function handleDragOver(e) {
e.preventDefault();
const imageUploadArea = document.getElementById('imageUploadArea');
if (imageUploadArea) imageUploadArea.classList.add('dragover');
}
function handleDragLeave(e) {
e.preventDefault();
const imageUploadArea = document.getElementById('imageUploadArea');
if (imageUploadArea) imageUploadArea.classList.remove('dragover');
}
function handleDrop(e) {
e.preventDefault();
const imageUploadArea = document.getElementById('imageUploadArea');
if (imageUploadArea) imageUploadArea.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0 && files[0].type.startsWith('image/')) {
handleImageFile(files[0]);
}
}
function handleImageSelect(e) {
const file = e.target.files[0];
if (file && file.type.startsWith('image/')) {
handleImageFile(file);
}
}
function handleImageFile(file) {
if (file.size > 50 * 1024 * 1024) {
showNotification('图片文件大小不能超过 50MB', 'error');
return;
}
const reader = new FileReader();
reader.onload = function(e) {
const previewImg = document.getElementById('previewImg');
const imagePreview = document.getElementById('imagePreview');
const uploadPlaceholder = document.querySelector('.upload-placeholder');
const imageUrl = document.getElementById('imageUrl');
if (previewImg) previewImg.src = e.target.result;
if (imagePreview) imagePreview.style.display = 'block';
if (uploadPlaceholder) uploadPlaceholder.style.display = 'none';
if (imageUrl) imageUrl.value = '';
};
reader.readAsDataURL(file);
}
function clearImagePreview() {
const imagePreview = document.getElementById('imagePreview');
const uploadPlaceholder = document.querySelector('.upload-placeholder');
const imageFile = document.getElementById('imageFile');
const imageUrl = document.getElementById('imageUrl');
if (imagePreview) imagePreview.style.display = 'none';
if (uploadPlaceholder) uploadPlaceholder.style.display = 'block';
if (imageFile) imageFile.value = '';
if (imageUrl) imageUrl.value = '';
}
// 表单提交处理
async function handleImageToVideoSubmit(e) {
e.preventDefault();
// 并发生成允许,不阻塞生成按钮
const prompt = document.getElementById('i2vPrompt')?.value.trim();
const imageUrl = document.getElementById('imageUrl')?.value.trim();
const imageFile = document.getElementById('imageFile');
const model = document.getElementById('i2vModel')?.value || 'seedance-pro-fast';
if (!prompt) {
showNotification('请输入文本描述', 'error');
return;
}
if (!imageFile?.files[0] && !imageUrl) {
showNotification('请选择图片或输入图片URL', 'error');
return;
}
const formData = new FormData();
if (imageFile?.files[0]) {
formData.append('image', imageFile.files[0]);
} else {
formData.append('image_url', imageUrl);
}
const apiKey = currentApiKey;
if (apiKey) {
formData.append('userApiKey', apiKey);
}
formData.append('prompt', prompt);
formData.append('model', model);
if (model === 'seedance-pro-fast') {
// Seedance 1.0 Pro Fast 所需参数
formData.append('aspect_ratio', document.getElementById('i2vSeedAspectRatio')?.value || 'auto');
formData.append('resolution', document.getElementById('i2vSeedResolution')?.value || '1080p');
formData.append('duration', document.getElementById('i2vSeedDuration')?.value || '5');
formData.append('camera_fixed', document.getElementById('i2vCameraFixed')?.checked ? 'true' : 'false');
formData.append('seed', (document.getElementById('i2vSeedValue')?.value ?? '-1').toString());
formData.append('enable_safety_checker', document.getElementById('i2vEnableSafety')?.checked ? 'true' : 'false');
} else {
// WAN v2.2-a14b 参数(保持原逻辑)
formData.append('negative_prompt', document.getElementById('i2vNegativePrompt')?.value || '');
formData.append('num_frames', document.getElementById('i2vFrames')?.value || '81');
formData.append('frames_per_second', document.getElementById('i2vFps')?.value || '16');
formData.append('resolution', document.getElementById('i2vResolution')?.value || '720p');
formData.append('aspect_ratio', document.getElementById('i2vAspectRatio')?.value || 'auto');
formData.append('video_quality', document.getElementById('i2vQuality')?.value || 'high');
formData.append('enable_safety_checker', document.getElementById('i2vDisableSafety')?.checked ? 'false' : 'true');
}
await generateVideo('/api/image-to-video', formData);
}
async function handleTextToVideoSubmit(e) {
e.preventDefault();
// 并发生成允许,不阻塞生成按钮
const prompt = document.getElementById('t2vPrompt')?.value.trim();
if (!prompt) {
showNotification('请输入文本描述', 'error');
return;
}
const model = document.getElementById('t2vModel')?.value || 'seedance-pro-fast';
const apiKey = currentApiKey;
if (model === 'seedance-pro-fast') {
// Bytedance Seedance 1.0 Pro Fast 入参
const requestData = {
prompt: prompt,
aspect_ratio: document.getElementById('seedAspectRatio')?.value || '16:9',
resolution: document.getElementById('seedResolution')?.value || '1080p',
duration: document.getElementById('seedDuration')?.value || '5',
camera_fixed: !!document.getElementById('t2vCameraFixed')?.checked,
seed: parseInt(document.getElementById('t2vSeed')?.value ?? '-1', 10),
enable_safety_checker: !!document.getElementById('t2vEnableSafety')?.checked,
model: 'seedance-pro-fast'
};
if (apiKey) {
requestData.userApiKey = apiKey;
}
await generateVideo('/api/text-to-video', requestData, 'json');
return;
}
// WAN v2.2-a14b 入参(保持原逻辑)
const requestData = {
prompt: prompt,
negative_prompt: document.getElementById('t2vNegativePrompt')?.value || '',
num_frames: parseInt(document.getElementById('t2vFrames')?.value || '81', 10),
frames_per_second: parseInt(document.getElementById('t2vFps')?.value || '16', 10),
resolution: document.getElementById('t2vResolution')?.value || '720p',
aspect_ratio: document.getElementById('t2vAspectRatio')?.value || '16:9',
video_quality: document.getElementById('t2vQuality')?.value || 'high',
enable_safety_checker: document.getElementById('t2vDisableSafety')?.checked ? false : true,
model: 'wan-v2.2-a14b'
};
if (apiKey) {
requestData.userApiKey = apiKey;
}
await generateVideo('/api/text-to-video', requestData, 'json');
}
// 视频生成
async function generateVideo(endpoint, data, contentType = 'form') {
// 在生成栏中创建队列项并显示进度,允许并发生成
const queueList = document.getElementById('queueList');
const queueCount = document.getElementById('queueCount');
if (!queueList) {
// 兼容旧页面:如果没有队列容器,直接执行旧逻辑的通知与结果展示
try {
const options = { method: 'POST' };
if (contentType === 'json') {
options.headers = { 'Content-Type': 'application/json' };
options.body = JSON.stringify(data);
} else {
options.body = data;
}
const response = await fetch(endpoint, options);
const result = await response.json();
if (result.success && result.data && result.data.video) {
showResult(result.data.video.url, result.data.prompt);
showNotification('视频生成成功!', 'success');
} else {
throw new Error(result.error || '视频生成失败');
}
} catch (error) {
console.error('生成错误:', error);
showNotification(error.message || '生成失败,请重试', 'error');
}
return;
}
// 若为空队列占位,先移除
if (queueList.classList.contains('empty')) {
queueList.classList.remove('empty');
queueList.innerHTML = '';
}
const taskId = `task-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
const title = endpoint.includes('image') ? '图片转视频' : '文本转视频';
const promptText = (() => {
if (typeof data === 'object' && contentType === 'json') {
return data.prompt ?? '';
}
// FormData 场景:直接从页面读取
const domId = endpoint.includes('image') ? 'i2vPrompt' : 't2vPrompt';
return document.getElementById(domId)?.value?.trim() ?? '';
})();
const item = document.createElement('div');
item.className = 'queue-item';
item.id = taskId;
item.innerHTML = `
<div class="info">
<div class="title">${title}</div>
<div class="prompt">${escapeHtml(promptText)}</div>
<div class="meta">${new Date().toLocaleString('zh-CN')}</div>
</div>
<div class="queue-progress">
<div class="progress-bar"><div class="progress-fill" id="${taskId}-progress"></div></div>
<div class="queue-status running" id="${taskId}-status">运行中</div>
</div>
`;
queueList.appendChild(item);
// 更新队列计数
if (queueCount) {
const count = queueList.querySelectorAll('.queue-item').length;
queueCount.textContent = `${count} 任务`;
}
// 每个任务独立的进度模拟(服务端暂不提供实时进度回传)
let progress = 0;
const progressEl = () => document.getElementById(`${taskId}-progress`);
const statusEl = () => document.getElementById(`${taskId}-status`);
const progressInterval = setInterval(() => {
progress += Math.random() * 15;
if (progress > 90) progress = 90;
if (progressEl()) progressEl().style.width = progress + '%';
}, 900);
try {
const options = { method: 'POST' };
if (contentType === 'json') {
options.headers = { 'Content-Type': 'application/json' };
options.body = JSON.stringify(data);
} else {
options.body = data;
}
const response = await fetch(endpoint, options);
const result = await response.json();
if (result.success && result.data && result.data.video) {
clearInterval(progressInterval);
if (progressEl()) progressEl().style.width = '100%';
if (statusEl()) {
statusEl().className = 'queue-status done';
statusEl().textContent = '完成';
}
showResult(result.data.video.url, result.data.prompt);
showNotification('视频生成成功!', 'success');
} else {
throw new Error(result.error || '视频生成失败');
}
} catch (error) {
clearInterval(progressInterval);
console.error('生成错误:', error);
if (statusEl()) {
statusEl().className = 'queue-status error';
statusEl().textContent = '错误';
}
if (error.message.includes('API') || error.message.includes('密钥')) {
showNotification('请先配置 API 密钥', 'error');
setTimeout(() => {
const apiKeyModal = document.getElementById('apiKeyModal');
if (apiKeyModal) apiKeyModal.style.display = 'flex';
}, 1000);
} else {
showNotification(error.message || '生成失败,请重试', 'error');
}
}
function escapeHtml(str) {
const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
return String(str || '').replace(/[&<>"']/g, s => map[s]);
}
}
// 加载状态
function showLoading() {
// 不再显示全屏遮罩,也不禁用按钮,避免占用全屏和阻塞生成键
const loadingContainer = document.getElementById('loadingContainer');
if (loadingContainer) loadingContainer.style.display = 'none';
}
function hideLoading() {
// 保持按钮可用,不做处理
const loadingContainer = document.getElementById('loadingContainer');
if (loadingContainer) loadingContainer.style.display = 'none';
}
function simulateProgress() {
// 已改为每个任务独立的进度显示,此处不再使用全局模拟
return;
}
// 结果显示
function showResult(videoUrl, prompt) {
const resultVideo = document.getElementById('resultVideo');
const downloadLink = document.getElementById('downloadLink');
const resultContainer = document.getElementById('resultContainer');
if (resultVideo) resultVideo.src = videoUrl;
if (downloadLink) {
downloadLink.href = videoUrl;
downloadLink.download = `generated-video-${Date.now()}.mp4`;
}
if (resultContainer) {
resultContainer.style.display = 'block';
resultContainer.scrollIntoView({ behavior: 'smooth' });
}
}
function clearResults() {
const resultContainer = document.getElementById('resultContainer');
const resultVideo = document.getElementById('resultVideo');
const downloadLink = document.getElementById('downloadLink');
if (resultContainer) resultContainer.style.display = 'none';
if (resultVideo) resultVideo.src = '';
if (downloadLink) downloadLink.href = '';
}
// 通知系统
function showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.innerHTML = `
<div class="notification-content">
<i class="fas ${getNotificationIcon(type)}"></i>
<span>${message}</span>
</div>
`;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${getNotificationColor(type)};
color: white;
padding: 1rem 1.5rem;
border-radius: 0.5rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1001;
transform: translateX(100%);
transition: transform 0.3s ease;
max-width: 400px;
`;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.transform = 'translateX(0)';
}, 100);
setTimeout(() => {
notification.style.transform = 'translateX(100%)';
setTimeout(() => {
if (document.body.contains(notification)) {
document.body.removeChild(notification);
}
}, 300);
}, 4000);
}
function getNotificationIcon(type) {
switch (type) {
case 'success': return 'fa-check-circle';
case 'error': return 'fa-exclamation-circle';
case 'warning': return 'fa-exclamation-triangle';
default: return 'fa-info-circle';
}
}
function getNotificationColor(type) {
switch (type) {
case 'success': return '#3b82f6';
case 'error': return '#ef4444';
case 'warning': return '#f59e0b';
default: return '#3b82f6';
}
}
// 视频库功能
async function loadVideoLibrary() {
const videoLibraryContent = document.getElementById('videoLibraryContent');
const videoCount = document.getElementById('videoCount');
if (!videoLibraryContent) return;
// 显示加载状态
videoLibraryContent.innerHTML = `
<div class="loading-library">
<i class="fas fa-spinner fa-spin"></i>
正在加载视频库...
</div>
`;
try {
const response = await fetch('/api/videos');
const data = await response.json();
if (data.videos && data.videos.length > 0) {
displayVideoLibrary(data.videos);
if (videoCount) {
videoCount.textContent = `共 ${data.videos.length} 个视频`;
}
} else {
displayEmptyLibrary();
if (videoCount) {
videoCount.textContent = '共 0 个视频';
}
}
} catch (error) {
console.error('加载视频库失败:', error);
videoLibraryContent.innerHTML = `
<div class="empty-library">
<i class="fas fa-exclamation-triangle"></i>
<p>加载视频库失败</p>
<button onclick="loadVideoLibrary()" class="refresh-button">重试</button>
</div>
`;
}
}
function displayVideoLibrary(videos) {
const videoLibraryContent = document.getElementById('videoLibraryContent');
const videoGrid = document.createElement('div');
videoGrid.className = 'video-grid';
videos.forEach(video => {
const videoItem = createVideoItem(video);
videoGrid.appendChild(videoItem);
});
videoLibraryContent.innerHTML = '';
videoLibraryContent.appendChild(videoGrid);
}
function createVideoItem(video) {
const item = document.createElement('div');
item.className = 'video-item';
const fileSize = formatFileSize(video.size);
const createdDate = new Date(video.created).toLocaleString('zh-CN');
item.innerHTML = `
<video class="video-preview" controls preload="metadata">
<source src="/${video.path}" type="video/mp4">
您的浏览器不支持视频播放。
</video>
<div class="video-info">
<div class="video-filename">${video.filename}</div>
<div class="video-meta">
<span>${createdDate}</span>
<span class="video-size">${fileSize}</span>
</div>
<div class="video-actions">
<button class="video-action-btn play-video-btn" onclick="playVideoInModal('/${video.path}')">
<i class="fas fa-play"></i>
播放
</button>
<button class="video-action-btn download-video-btn" onclick="downloadVideo('/${video.path}', '${video.filename}')">
<i class="fas fa-download"></i>
下载
</button>
</div>
</div>
`;
return item;
}
function displayEmptyLibrary() {
const videoLibraryContent = document.getElementById('videoLibraryContent');
videoLibraryContent.innerHTML = `
<div class="empty-library">
<i class="fas fa-video"></i>
<p>暂无视频</p>
<p class="upload-hint">生成的视频会自动保存到这里</p>
</div>
`;
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function playVideoInModal(videoPath) {
// 在结果容器中播放视频
const resultVideo = document.getElementById('resultVideo');
const resultContainer = document.getElementById('resultContainer');
const downloadLink = document.getElementById('downloadLink');
if (resultVideo && resultContainer && downloadLink) {
resultVideo.src = videoPath;
downloadLink.href = videoPath;
downloadLink.download = videoPath.split('/').pop();
resultContainer.style.display = 'block';
resultContainer.scrollIntoView({ behavior: 'smooth' });
}
}
function downloadVideo(videoPath, filename) {
const link = document.createElement('a');
link.href = videoPath;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showNotification('开始下载视频', 'success');
}
// 修改 showResult 函数,生成视频后自动刷新视频库
function showResult(videoUrl, prompt) {
const resultVideo = document.getElementById('resultVideo');
const downloadLink = document.getElementById('downloadLink');
const resultContainer = document.getElementById('resultContainer');
if (resultVideo) resultVideo.src = videoUrl;
if (downloadLink) {
downloadLink.href = videoUrl;
downloadLink.download = `generated-video-${Date.now()}.mp4`;
}
if (resultContainer) {
resultContainer.style.display = 'block';
resultContainer.scrollIntoView({ behavior: 'smooth' });
}
// 刷新视频库
setTimeout(() => {
loadVideoLibrary();
}, 2000);
}
// 错误处理
window.addEventListener('error', function(e) {
console.error('全局错误:', e.error);
if (isGenerating) {
hideLoading();
showNotification('发生未知错误,请重试', 'error');
isGenerating = false;
}
});
window.addEventListener('online', () => {
showNotification('网络连接已恢复', 'success');
});
window.addEventListener('offline', () => {
showNotification('网络连接已断开', 'warning');
});