// 全局变量 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 = ' 保存中...'; } 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 = ' 保存设置'; } } 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 = ' 连接中...'; 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 = ' 测试连接'; } } // 标签页切换 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 = `
加载视频库失败
暂无视频
生成的视频会自动保存到这里