import { ref, computed, watch, nextTick } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import i18n from './i18n' import router from '../router' export const t = i18n.global.t export const locale = i18n.global.locale // 响应式数据 const loading = ref(false); const loginLoading = ref(false); const initLoading = ref(false); const downloadLoading = ref(false); const isLoading = ref(false); // 页面加载loading状态 // 录音相关状态 const isRecording = ref(false); const mediaRecorder = ref(null); const audioChunks = ref([]); const recordingDuration = ref(0); const recordingTimer = ref(null); const alert = ref({ show: false, message: '', type: 'info' }); // 短信登录相关数据 const phoneNumber = ref(''); const verifyCode = ref(''); const smsCountdown = ref(0); const showSmsForm = ref(false); const showErrorDetails = ref(false); const showFailureDetails = ref(false); // 任务类型下拉菜单 const showTaskTypeMenu = ref(false); const showModelMenu = ref(false); // 任务状态轮询相关 const pollingInterval = ref(null); const pollingTasks = ref(new Set()); // 正在轮询的任务ID集合 const confirmDialog = ref({ show: false, title: '', message: '', confirmText: '确认', // 使用静态文本,避免翻译依赖 warning: null, confirm: () => { } }); const submitting = ref(false); const templateLoading = ref(false); // 模板加载状态 const taskSearchQuery = ref(''); const sidebarCollapsed = ref(false); const showExpandHint = ref(false); const showGlow = ref(false); const isDefaultStateHidden = ref(false); const isCreationAreaExpanded = ref(false); const hasUploadedContent = ref(false); const isContracting = ref(false); const showTaskDetailModal = ref(false); const modalTask = ref(null); // 视频加载状态跟踪 const videoLoadedStates = ref(new Map()); // 跟踪每个视频的加载状态 // 检查视频是否已加载完成 const isVideoLoaded = (videoSrc) => { return videoLoadedStates.value.get(videoSrc) || false; }; // 设置视频加载状态 const setVideoLoaded = (videoSrc, loaded) => { videoLoadedStates.value.set(videoSrc, loaded); }; // 灵感广场相关数据 const inspirationSearchQuery = ref(''); const selectedInspirationCategory = ref(''); const inspirationItems = ref([]); const InspirationCategories = ref([]); // 灵感广场分页相关变量 const inspirationPagination = ref(null); const inspirationCurrentPage = ref(1); const inspirationPageSize = ref(12); const inspirationPageInput = ref(1); const inspirationPaginationKey = ref(0); // 模板详情弹窗相关数据 const showTemplateDetailModal = ref(false); const selectedTemplate = ref(null); // 图片放大弹窗相关数据 const showImageZoomModal = ref(false); const zoomedImageUrl = ref(''); // 任务文件缓存系统 const taskFileCache = ref(new Map()); const taskFileCacheLoaded = ref(false); // 模板文件缓存系统 const templateFileCache = ref(new Map()); const templateFileCacheLoaded = ref(false); // 防重复获取的状态管理 const templateUrlFetching = ref(new Set()); // 正在获取的URL集合 const taskUrlFetching = ref(new Map()); // 正在获取的任务URL集合 // localStorage缓存相关常量 const TASK_FILE_CACHE_KEY = 'lightx2v_task_files'; const TEMPLATE_FILE_CACHE_KEY = 'lightx2v_template_files'; const TASK_FILE_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 24小时过期 const MODELS_CACHE_KEY = 'lightx2v_models'; const MODELS_CACHE_EXPIRY = 60 * 60 * 1000; // 1小时过期 const TEMPLATES_CACHE_KEY = 'lightx2v_templates'; const TEMPLATES_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 24小时过期 const TASKS_CACHE_KEY = 'lightx2v_tasks'; const TASKS_CACHE_EXPIRY = 5 * 60 * 1000; // 5分钟过期 const imageTemplates = ref([]); const audioTemplates = ref([]); const showImageTemplates = ref(false); const showAudioTemplates = ref(false); const mediaModalTab = ref('history'); // Template分页相关变量 const templatePagination = ref(null); const templateCurrentPage = ref(1); const templatePageSize = ref(12); // 图片模板每页12个,音频模板每页10个 const templatePageInput = ref(1); const templatePaginationKey = ref(0); const imageHistory = ref([]); const audioHistory = ref([]); // 模板文件缓存,避免重复下载 const currentUser = ref({}); const models = ref([]); const tasks = ref([]); const isLoggedIn = ref(null); // null表示未初始化,false表示未登录,true表示已登录 const selectedTaskId = ref(null); const selectedTask = ref(null); const selectedModel = ref(null); const selectedTaskFiles = ref({ inputs: {}, outputs: {} }); // 存储任务的输入输出文件 const loadingTaskFiles = ref(false); // 加载任务文件的状态 const statusFilter = ref('ALL'); const pagination = ref(null); const currentTaskPage = ref(1); const taskPageSize = ref(12); const taskPageInput = ref(1); const paginationKey = ref(0); // 用于强制刷新分页组件 const taskMenuVisible = ref({}); // 管理每个任务的菜单显示状态 const nameMap = computed(() => ({ 't2v': t('textToVideo'), 'i2v': t('imageToVideo'), 's2v': t('speechToVideo') })); // 任务类型提示信息 const taskHints = computed(() => ({ 't2v': [ t('t2vHint1'), t('t2vHint2'), t('t2vHint3'), t('t2vHint4') ], 'i2v': [ t('i2vHint1'), t('i2vHint2'), t('i2vHint3'), t('i2vHint4') ], 's2v': [ t('s2vHint1'), t('s2vHint2'), t('s2vHint3'), t('s2vHint4') ] })); // 当前任务类型的提示信息 const currentTaskHints = computed(() => { return taskHints.value[selectedTaskId.value] || taskHints.value['s2v']; }); // 滚动提示相关 const currentHintIndex = ref(0); const hintInterval = ref(null); // 开始滚动提示 const startHintRotation = () => { if (hintInterval.value) { clearInterval(hintInterval.value); } hintInterval.value = setInterval(() => { currentHintIndex.value = (currentHintIndex.value + 1) % currentTaskHints.value.length; }, 3000); // 每3秒切换一次 }; // 停止滚动提示 const stopHintRotation = () => { if (hintInterval.value) { clearInterval(hintInterval.value); hintInterval.value = null; } }; // 为三个任务类型分别创建独立的表单 const t2vForm = ref({ task: 't2v', model_cls: '', stage: 'single_stage', prompt: '', seed: 42 }); const i2vForm = ref({ task: 'i2v', model_cls: '', stage: 'multi_stage', imageFile: null, prompt: '', seed: 42 }); const s2vForm = ref({ task: 's2v', model_cls: '', stage: 'single_stage', imageFile: null, audioFile: null, prompt: '', seed: 42 }); // 根据当前选择的任务类型获取对应的表单 const getCurrentForm = () => { switch (selectedTaskId.value) { case 't2v': return t2vForm.value; case 'i2v': return i2vForm.value; case 's2v': return s2vForm.value; default: return t2vForm.value; } }; // 控制默认状态显示/隐藏的方法 const hideDefaultState = () => { isDefaultStateHidden.value = true; }; const showDefaultState = () => { isDefaultStateHidden.value = false; }; // 控制创作区域展开/收缩的方法 const expandCreationArea = () => { isCreationAreaExpanded.value = true; // 添加show类来触发动画 setTimeout(() => { const creationArea = document.querySelector('.creation-area'); if (creationArea) { creationArea.classList.add('show'); } }, 10); }; const contractCreationArea = () => { isContracting.value = true; const creationArea = document.querySelector('.creation-area'); if (creationArea) { // 添加hide类来触发收起动画 creationArea.classList.add('hide'); creationArea.classList.remove('show'); } // 等待动画完成后更新状态 setTimeout(() => { isCreationAreaExpanded.value = false; isContracting.value = false; if (creationArea) { creationArea.classList.remove('hide'); } }, 400); }; // 为每个任务类型创建独立的预览变量 const i2vImagePreview = ref(null); const s2vImagePreview = ref(null); const s2vAudioPreview = ref(null); // 监听上传内容变化 const updateUploadedContentStatus = () => { hasUploadedContent.value = !!(getCurrentImagePreview() || getCurrentAudioPreview() || getCurrentForm().prompt?.trim()); }; // 监听表单变化 watch([i2vImagePreview, s2vImagePreview, s2vAudioPreview, () => getCurrentForm().prompt], () => { updateUploadedContentStatus(); }, { deep: true }); // 监听任务类型变化,重置提示滚动 watch(selectedTaskId, () => { currentHintIndex.value = 0; stopHintRotation(); startHintRotation(); }); // 根据当前任务类型获取对应的预览变量 const getCurrentImagePreview = () => { switch (selectedTaskId.value) { case 't2v': return null; case 'i2v': return i2vImagePreview.value; case 's2v': return s2vImagePreview.value; default: return null; } }; const getCurrentAudioPreview = () => { switch (selectedTaskId.value) { case 't2v': return null case 'i2v': return null case 's2v': return s2vAudioPreview.value; default: return null; } }; const setCurrentImagePreview = (value) => { switch (selectedTaskId.value) { case 't2v': break; case 'i2v': i2vImagePreview.value = value; break; case 's2v': s2vImagePreview.value = value; break; } // 清除图片预览缓存,确保新图片能正确显示 urlCache.value.delete('current_image_preview'); }; const setCurrentAudioPreview = (value) => { switch (selectedTaskId.value) { case 't2v': break; case 'i2v': break; case 's2v': s2vAudioPreview.value = value; break; } // 清除音频预览缓存,确保新音频能正确显示 urlCache.value.delete('current_audio_preview'); }; // 提示词模板相关 const showTemplates = ref(false); const showHistory = ref(false); const showPromptModal = ref(false); const promptModalTab = ref('templates'); // 计算属性 const availableTaskTypes = computed(() => { const types = [...new Set(models.value.map(m => m.task))]; // 重新排序,确保数字人在最左边 const orderedTypes = []; // 检查是否有s2v模型,如果有则添加s2v类型 const hasS2vModels = models.value.some(m => m.task === 's2v' ); // 优先添加数字人(如果存在相关模型) if (hasS2vModels) { orderedTypes.push('s2v'); } // 然后添加其他类型 types.forEach(type => { if (type !== 's2v') { orderedTypes.push(type); } }); return orderedTypes; }); const availableModelClasses = computed(() => { if (!selectedTaskId.value) return []; return [...new Set(models.value .filter(m => m.task === selectedTaskId.value) .map(m => m.model_cls))]; }); const filteredTasks = computed(() => { let filtered = tasks.value; // 状态过滤 if (statusFilter.value !== 'ALL') { filtered = filtered.filter(task => task.status === statusFilter.value); } // 搜索过滤 if (taskSearchQuery.value) { filtered = filtered.filter(task => task.params.prompt?.toLowerCase().includes(taskSearchQuery.value.toLowerCase()) || task.task_id.toLowerCase().includes(taskSearchQuery.value.toLowerCase()) || nameMap.value[task.task_type].toLowerCase().includes(taskSearchQuery.value.toLowerCase()) ); } // 按时间排序,最新的任务在前面 filtered = filtered.sort((a, b) => { const timeA = parseInt(a.create_t) || 0; const timeB = parseInt(b.create_t) || 0; return timeB - timeA; // 降序排列,最新的在前 }); return filtered; }); // 监听状态筛选变化,重置分页到第一页 watch(statusFilter, (newStatus, oldStatus) => { if (newStatus !== oldStatus) { currentTaskPage.value = 1; taskPageInput.value = 1; refreshTasks(true); // 强制刷新 } }); // 监听搜索查询变化,重置分页到第一页 watch(taskSearchQuery, (newQuery, oldQuery) => { if (newQuery !== oldQuery) { currentTaskPage.value = 1; taskPageInput.value = 1; refreshTasks(true); // 强制刷新 } }); // 分页信息计算属性,确保响应式更新 const paginationInfo = computed(() => { if (!pagination.value) return null; return { total: pagination.value.total || 0, total_pages: pagination.value.total_pages || 0, current_page: pagination.value.current_page || currentTaskPage.value, page_size: pagination.value.page_size || taskPageSize.value }; }); // Template分页信息计算属性 const templatePaginationInfo = computed(() => { if (!templatePagination.value) return null; return { total: templatePagination.value.total || 0, total_pages: templatePagination.value.total_pages || 0, current_page: templatePagination.value.current_page || templateCurrentPage.value, page_size: templatePagination.value.page_size || templatePageSize.value }; }); // 灵感广场分页信息计算属性 const inspirationPaginationInfo = computed(() => { if (!inspirationPagination.value) return null; return { total: inspirationPagination.value.total || 0, total_pages: inspirationPagination.value.total_pages || 0, current_page: inspirationPagination.value.current_page || inspirationCurrentPage.value, page_size: inspirationPagination.value.page_size || inspirationPageSize.value }; }); // 通用URL缓存 const urlCache = ref(new Map()); // 通用URL缓存函数 const getCachedUrl = (key, urlGenerator) => { if (urlCache.value.has(key)) { return urlCache.value.get(key); } const url = urlGenerator(); urlCache.value.set(key, url); return url; }; // 获取历史图片URL(带缓存) const getHistoryImageUrl = (history) => { if (!history || !history.thumbnail) return ''; return getCachedUrl(`history_image_${history.filename}`, () => history.thumbnail); }; // 获取用户头像URL(带缓存) const getUserAvatarUrl = (user) => { if (!user || !user.avatar) return ''; return getCachedUrl(`user_avatar_${user.username}`, () => user.avatar); }; // 获取当前图片预览URL(带缓存) const getCurrentImagePreviewUrl = () => { const preview = getCurrentImagePreview(); if (!preview) return ''; return getCachedUrl(`current_image_preview`, () => preview); }; // 获取当前音频预览URL(带缓存) const getCurrentAudioPreviewUrl = () => { const preview = getCurrentAudioPreview(); if (!preview) return ''; return getCachedUrl(`current_audio_preview`, () => preview); }; // 方法 const showAlert = (message, type = 'info') => { alert.value = { show: true, message, type }; setTimeout(() => { alert.value.show = false; }, 5000); }; // 显示确认对话框 const showConfirmDialog = (options) => { return new Promise((resolve) => { confirmDialog.value = { show: true, title: options.title || '确认操作', message: options.message || '确定要执行此操作吗?', confirmText: options.confirmText || '确认', warning: options.warning || null, confirm: () => { confirmDialog.value.show = false; resolve(true); }, cancel: () => { confirmDialog.value.show = false; resolve(false); } }; }); }; const setLoading = (value) => { loading.value = value; }; const apiCall = async (endpoint, options = {}) => { const url = `${endpoint}`; const headers = { 'Content-Type': 'application/json', ...options.headers }; if (localStorage.getItem('accessToken')) { headers['Authorization'] = `Bearer ${localStorage.getItem('accessToken')}`; } const response = await fetch(url, { ...options, headers }); if (response.status === 401) { logout(); throw new Error('认证失败,请重新登录'); } if (response.status === 400) { const error = await response.json(); showAlert(error.message, 'danger'); throw new Error(error.message); } // 添加50ms延迟,防止触发服务端频率限制 await new Promise(resolve => setTimeout(resolve, 50)); return response; }; const loginWithGitHub = async () => { try { console.log('starting GitHub login') const response = await fetch('/auth/login/github'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); localStorage.setItem('loginSource', 'github'); window.location.href = data.auth_url; } catch (error) { console.log('GitHub login error:', error); showAlert('获取GitHub认证URL失败', 'danger'); } }; const loginWithGoogle = async () => { try { console.log('starting Google login') const response = await fetch('/auth/login/google'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); localStorage.setItem('loginSource', 'google'); window.location.href = data.auth_url; } catch (error) { console.error('Google login error:', error); showAlert('获取Google认证URL失败', 'danger'); } }; // 发送短信验证码 const sendSmsCode = async () => { if (!phoneNumber.value) { showAlert('手机号', 'warning'); return; } // 简单的手机号格式验证 const phoneRegex = /^1[3-9]\d{9}$/; if (!phoneRegex.test(phoneNumber.value)) { showAlert('请输入正确的手机号格式', 'warning'); return; } try { const response = await fetch(`./auth/login/sms?phone_number=${phoneNumber.value}`); const data = await response.json(); if (response.ok) { showAlert('验证码已发送,请查收短信', 'success'); // 开始倒计时 startSmsCountdown(); } else { showAlert(data.message || '发送验证码失败', 'danger'); } } catch (error) { showAlert('发送验证码失败,请重试', 'danger'); } }; // 短信验证码登录 const loginWithSms = async () => { if (!phoneNumber.value || !verifyCode.value) { showAlert('请输入手机号和验证码', 'warning'); return; } try { const response = await fetch(`./auth/callback/sms?phone_number=${phoneNumber.value}&verify_code=${verifyCode.value}`); const data = await response.json(); if (response.ok) { localStorage.setItem('accessToken', data.access_token); localStorage.setItem('currentUser', JSON.stringify(data.user_info)); currentUser.value = data.user_info; // 登录成功后初始化数据 await init(); router.push('/generate'); console.log('login with sms success'); isLoggedIn.value = true; showAlert('登录成功', 'success'); } else { showAlert(data.message || '验证码错误或已过期', 'danger'); } } catch (error) { showAlert('登录失败,请重试', 'danger'); } }; // 处理手机号输入框回车键 const handlePhoneEnter = () => { if (phoneNumber.value && !smsCountdown.value) { sendSmsCode(); } }; // 处理验证码输入框回车键 const handleVerifyCodeEnter = () => { if (phoneNumber.value && verifyCode.value) { loginWithSms(); } }; // 移动端检测和样式应用 const applyMobileStyles = () => { if (window.innerWidth <= 640) { // 为左侧功能区添加移动端样式 const leftNav = document.querySelector('.relative.w-20.pl-5.flex.flex-col.z-10'); if (leftNav) { leftNav.classList.add('mobile-bottom-nav'); } // 为导航按钮容器添加移动端样式 const navContainer = document.querySelector('.p-2.flex.flex-col.justify-center.h-full'); if (navContainer) { navContainer.classList.add('mobile-nav-buttons'); } // 为所有导航按钮添加移动端样式 const navButtons = document.querySelectorAll('.relative.w-20.pl-5.flex.flex-col.z-10 button'); navButtons.forEach(btn => { btn.classList.add('mobile-nav-btn'); }); // 为主内容区域添加移动端样式 const contentAreas = document.querySelectorAll('.flex-1.flex.flex-col.min-h-0'); contentAreas.forEach(area => { area.classList.add('mobile-content'); }); } }; // 短信验证码倒计时 const startSmsCountdown = () => { smsCountdown.value = 60; const timer = setInterval(() => { smsCountdown.value--; if (smsCountdown.value <= 0) { clearInterval(timer); } }, 1000); }; // 切换短信登录表单显示 const toggleSmsLogin = () => { showSmsForm.value = !showSmsForm.value; if (!showSmsForm.value) { // 重置表单数据 phoneNumber.value = ''; verifyCode.value = ''; smsCountdown.value = 0; } }; const handleLoginCallback = async (code, source) => { try { const response = await fetch(`/auth/callback/${source}?code=${code}`); if (response.ok) { const data = await response.json(); console.log(data); localStorage.setItem('accessToken', data.access_token); localStorage.setItem('currentUser', JSON.stringify(data.user_info)); currentUser.value = data.user_info; isLoggedIn.value = true; // 在进入新页面前显示loading isLoading.value = true; // 登录成功后初始化数据 await init(); // 检查是否有分享数据需要导入 const shareData = localStorage.getItem('shareData'); if (shareData) { // 解析分享数据获取shareId try { const parsedShareData = JSON.parse(shareData); const shareId = parsedShareData.share_id || parsedShareData.task_id; if (shareId) { localStorage.removeItem('shareData'); // 跳转回分享页面,让createSimilar函数处理数据 router.push(`/share/${shareId}`); return; } } catch (error) { console.warn('Failed to parse share data:', error); } localStorage.removeItem('shareData'); } // 默认跳转到生成页面 router.push('/generate'); console.log('login with callback success'); // 清除URL中的code参数 window.history.replaceState({}, document.title, window.location.pathname); } else { const error = await response.json(); showAlert(`登录失败: ${error.detail}`, 'danger'); } } catch (error) { showAlert('登录过程中发生错误', 'danger'); console.error(error); } }; const logout = () => { localStorage.removeItem('accessToken'); localStorage.removeItem('currentUser'); clearAllCache(); switchToLoginView(); isLoggedIn.value = false; models.value = []; tasks.value = []; showAlert('已退出登录', 'info'); }; const login = () => { switchToLoginView(); isLoggedIn.value = false; }; const loadModels = async (forceRefresh = false) => { try { // 如果不是强制更新,先尝试从缓存加载 if (!forceRefresh) { const cachedModels = loadFromCache(MODELS_CACHE_KEY, MODELS_CACHE_EXPIRY); if (cachedModels) { console.log('成功从缓存加载模型列表'); models.value = cachedModels; return; } } console.log('开始加载模型列表...'); const response = await apiRequest('/api/v1/model/list'); if (response && response.ok) { const data = await response.json(); console.log('模型列表数据:', data); const modelsData = data.models || []; models.value = modelsData; // 保存到缓存 saveToCache(MODELS_CACHE_KEY, modelsData); console.log('模型列表已缓存'); } else if (response) { console.error('模型列表API响应失败:', response); showAlert('加载模型列表失败', 'danger'); } // 如果response为null,说明是认证错误,apiRequest已经处理了 } catch (error) { console.error('加载模型失败:', error); showAlert(`加载模型失败: ${error.message}`, 'danger'); } }; const refreshTemplateFileUrl = (templatesData) => { for (const img of templatesData.images) { console.log('刷新图片素材文件URL:', img.filename, img.url); setTemplateFileToCache(img.filename, {url: img.url, timestamp: Date.now()}); } for (const audio of templatesData.audios) { console.log('刷新音频素材文件URL:', audio.filename, audio.url); setTemplateFileToCache(audio.filename, {url: audio.url, timestamp: Date.now()}); } for (const video of templatesData.videos) { console.log('刷新视频素材文件URL:', video.filename, video.url); setTemplateFileToCache(video.filename, {url: video.url, timestamp: Date.now()}); } } // 加载模板文件 const loadImageAudioTemplates = async (forceRefresh = false) => { try { // 如果不是强制刷新,先尝试从缓存加载 const cacheKey = `${TEMPLATES_CACHE_KEY}_IMAGE_AUDIO_${templateCurrentPage.value}_${templatePageSize.value}`; if (!forceRefresh) { // 构建缓存键,包含分页和过滤条件 const cachedTemplates = loadFromCache(cacheKey, TEMPLATES_CACHE_EXPIRY); if (cachedTemplates && cachedTemplates.templates) { console.log('成功从缓存加载模板列表'); imageTemplates.value = cachedTemplates.templates.images || []; audioTemplates.value = cachedTemplates.templates.audios || []; templatePagination.value = cachedTemplates.pagination || null; return; } } console.log('开始加载图片音乐素材库...'); const response = await publicApiCall(`/api/v1/template/list?page=${templateCurrentPage.value}&page_size=${templatePageSize.value}`); if (response.ok) { const data = await response.json(); console.log('图片音乐素材库数据:', data); refreshTemplateFileUrl(data.templates); const templatesData = { images: data.templates.images || [], audios: data.templates.audios || [] }; imageTemplates.value = templatesData.images; audioTemplates.value = templatesData.audios; templatePagination.value = data.pagination || null; // 保存到缓存 saveToCache(cacheKey, { templates: templatesData, pagination: templatePagination.value }); console.log('图片音乐素材库已缓存:', templatesData); } else { console.warn('加载素材库失败'); } } catch (error) { console.warn('加载素材库失败:', error); } }; // 获取素材文件的通用函数(带缓存) const getTemplateFile = async (template) => { const cacheKey = template.url; // 先检查内存缓存 if (templateFileCache.value.has(cacheKey)) { console.log('从内存缓存获取素材文件:', template.filename); return templateFileCache.value.get(cacheKey); } // 如果缓存中没有,则下载并缓存 console.log('下载素材文件:', template.filename); const response = await fetch(template.url, { cache: 'force-cache' // 强制使用浏览器缓存 }); if (response.ok) { const blob = await response.blob(); // 根据文件扩展名确定正确的MIME类型 let mimeType = blob.type; const extension = template.filename.toLowerCase().split('.').pop(); if (extension === 'wav') { mimeType = 'audio/wav'; } else if (extension === 'mp3') { mimeType = 'audio/mpeg'; } else if (extension === 'm4a') { mimeType = 'audio/mp4'; } else if (extension === 'ogg') { mimeType = 'audio/ogg'; } else if (extension === 'webm') { mimeType = 'audio/webm'; } console.log('文件扩展名:', extension, 'MIME类型:', mimeType); const file = new File([blob], template.filename, { type: mimeType }); // 缓存文件对象 templateFileCache.value.set(cacheKey, file); console.log('下载素材文件完成:', template.filename); return file; } else { throw new Error('下载素材文件失败'); } }; // 选择图片素材 const selectImageTemplate = async (template) => { try { const file = await getTemplateFile(template); if (selectedTaskId.value === 'i2v') { i2vForm.value.imageFile = file; } else if (selectedTaskId.value === 's2v') { s2vForm.value.imageFile = file; } // 创建预览 const reader = new FileReader(); reader.onload = (e) => { setCurrentImagePreview(e.target.result); }; reader.readAsDataURL(file); showImageTemplates.value = false; showAlert('图片素材已选择', 'success'); } catch (error) { showAlert(`加载图片素材失败: ${error.message}`, 'danger'); } }; // 选择音频素材 const selectAudioTemplate = async (template) => { try { const file = await getTemplateFile(template); s2vForm.value.audioFile = file; // 创建预览 const reader = new FileReader(); reader.onload = (e) => { setCurrentAudioPreview(e.target.result); updateUploadedContentStatus(); }; reader.readAsDataURL(file); showAudioTemplates.value = false; showAlert('音频素材已选择', 'success'); } catch (error) { showAlert(`加载音频素材失败: ${error.message}`, 'danger'); } }; // 预览音频素材 const previewAudioTemplate = (template) => { console.log('预览音频模板:', template); const audioUrl = getTemplateFileUrl(template.filename, 'audios'); console.log('音频URL:', audioUrl); if (!audioUrl) { showAlert('音频文件URL获取失败', 'danger'); return; } const audio = new Audio(audioUrl); audio.play().catch(error => { console.error('音频播放失败:', error); showAlert('音频播放失败', 'danger'); }); }; const handleImageUpload = (event) => { const file = event.target.files[0]; if (file) { if (selectedTaskId.value === 'i2v') { i2vForm.value.imageFile = file; } else if (selectedTaskId.value === 's2v') { s2vForm.value.imageFile = file; } const reader = new FileReader(); reader.onload = (e) => { setCurrentImagePreview(e.target.result); updateUploadedContentStatus(); }; reader.readAsDataURL(file); } else { // 用户取消了选择,保持原有图片不变 // 不做任何操作 } }; const selectTask = (taskType) => { selectedTaskId.value = taskType; // 根据任务类型恢复对应的预览 if (taskType === 'i2v' && i2vForm.value.imageFile) { // 恢复图片预览 const reader = new FileReader(); reader.onload = (e) => { setCurrentImagePreview(e.target.result); }; reader.readAsDataURL(i2vForm.value.imageFile); } else if (taskType === 's2v') { // 恢复数字人任务的图片和音频预览 if (s2vForm.value.imageFile) { const reader = new FileReader(); reader.onload = (e) => { setCurrentImagePreview(e.target.result); }; reader.readAsDataURL(s2vForm.value.imageFile); } if (s2vForm.value.audioFile) { const reader = new FileReader(); reader.onload = (e) => { setCurrentAudioPreview(e.target.result); }; reader.readAsDataURL(s2vForm.value.audioFile); } } // 如果当前表单没有选择模型,自动选择第一个可用的模型 const currentForm = getCurrentForm(); if (!currentForm.model_cls) { const availableModels = models.value.filter(m => m.task === taskType); if (availableModels.length > 0) { const firstModel = availableModels[0]; currentForm.model_cls = firstModel.model_cls; currentForm.stage = firstModel.stage; } } }; const selectModel = (model) => { selectedModel.value = model; getCurrentForm().model_cls = model; }; const triggerImageUpload = () => { document.querySelector('input[type="file"][accept="image/*"]').click(); }; const triggerAudioUpload = () => { const audioInput = document.querySelector('input[type="file"][accept="audio/*"]'); if (audioInput) { audioInput.click(); } else { console.warn('音频输入框未找到'); } }; const removeImage = () => { setCurrentImagePreview(null); if (selectedTaskId.value === 'i2v') { i2vForm.value.imageFile = null; } else if (selectedTaskId.value === 's2v') { s2vForm.value.imageFile = null; } updateUploadedContentStatus(); // 重置文件输入框,确保可以重新选择相同文件 const imageInput = document.querySelector('input[type="file"][accept="image/*"]'); if (imageInput) { imageInput.value = ''; } }; const removeAudio = () => { setCurrentAudioPreview(null); s2vForm.value.audioFile = null; updateUploadedContentStatus(); console.log('音频已移除'); // 重置音频文件输入框,确保可以重新选择相同文件 const audioInput = document.querySelector('input[type="file"][accept="audio/*"]'); if (audioInput) { audioInput.value = ''; } }; const getAudioMimeType = () => { if (s2vForm.value.audioFile) { return s2vForm.value.audioFile.type; } return 'audio/mpeg'; // 默认类型 }; const handleAudioUpload = (event) => { const file = event.target.files[0]; if (file) { s2vForm.value.audioFile = file; const reader = new FileReader(); reader.onload = (e) => { setCurrentAudioPreview(e.target.result); updateUploadedContentStatus(); console.log('音频预览已设置:', e.target.result); }; reader.readAsDataURL(file); } else { setCurrentAudioPreview(null); updateUploadedContentStatus(); } }; // 开始录音 const startRecording = async () => { try { console.log('开始录音...'); // 检查浏览器支持 if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { throw new Error('浏览器不支持录音功能'); } if (!window.MediaRecorder) { throw new Error('浏览器不支持MediaRecorder'); } console.log('浏览器支持检查通过,请求麦克风权限...'); // 请求麦克风权限 const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, sampleRate: 44100 } }); // 创建MediaRecorder mediaRecorder.value = new MediaRecorder(stream, { mimeType: 'audio/webm;codecs=opus' }); audioChunks.value = []; // 监听数据可用事件 mediaRecorder.value.ondataavailable = (event) => { if (event.data.size > 0) { audioChunks.value.push(event.data); } }; // 监听录音停止事件 mediaRecorder.value.onstop = () => { const audioBlob = new Blob(audioChunks.value, { type: 'audio/webm' }); const audioFile = new File([audioBlob], 'recording.webm', { type: 'audio/webm' }); // 设置到表单 s2vForm.value.audioFile = audioFile; // 创建预览URL const audioUrl = URL.createObjectURL(audioBlob); setCurrentAudioPreview(audioUrl); updateUploadedContentStatus(); // 停止所有音频轨道 stream.getTracks().forEach(track => track.stop()); showAlert(t('recordingCompleted'), 'success'); }; // 开始录音 mediaRecorder.value.start(1000); // 每秒收集一次数据 isRecording.value = true; recordingDuration.value = 0; // 开始计时 recordingTimer.value = setInterval(() => { recordingDuration.value++; }, 1000); showAlert(t('recordingStarted'), 'info'); } catch (error) { console.error('录音失败:', error); let errorMessage = t('recordingFailed'); if (error.name === 'NotAllowedError') { errorMessage = '麦克风权限被拒绝,请在浏览器设置中允许麦克风访问'; } else if (error.name === 'NotFoundError') { errorMessage = '未找到麦克风设备'; } else if (error.name === 'NotSupportedError') { errorMessage = '浏览器不支持录音功能,请使用HTTPS访问'; } else if (error.message) { errorMessage = error.message; } showAlert(errorMessage, 'danger'); } }; // 停止录音 const stopRecording = () => { if (mediaRecorder.value && isRecording.value) { mediaRecorder.value.stop(); isRecording.value = false; if (recordingTimer.value) { clearInterval(recordingTimer.value); recordingTimer.value = null; } showAlert(t('recordingStopped'), 'info'); } }; // 格式化录音时长 const formatRecordingDuration = (seconds) => { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; }; const submitTask = async () => { try { // 检查是否正在加载模板 if (templateLoading.value) { showAlert('模板正在加载中,请稍后再试', 'warning'); return; } const currentForm = getCurrentForm(); // 表单验证 if (!selectedTaskId.value) { showAlert('请选择任务类型', 'warning'); return; } if (!currentForm.model_cls) { showAlert('请选择模型', 'warning'); return; } if (!currentForm.prompt || currentForm.prompt.trim().length === 0) { if (selectedTaskId.value === 's2v') { currentForm.prompt = 'Make the character speak in a natural way according to the audio.'; } else { showAlert('请输入提示词', 'warning'); return; } } if (currentForm.prompt.length > 1000) { showAlert('提示词长度不能超过1000个字符', 'warning'); return; } if (selectedTaskId.value === 'i2v' && !currentForm.imageFile) { showAlert('图生视频任务需要上传参考图片', 'warning'); return; } if (selectedTaskId.value === 's2v' && !currentForm.imageFile) { showAlert('数字人任务需要上传参考图片', 'warning'); return; } if (selectedTaskId.value === 's2v' && !currentForm.audioFile) { showAlert('数字人任务需要上传音频文件', 'warning'); return; } submitting.value = true; // 确定实际提交的任务类型 let actualTaskType = selectedTaskId.value; var formData = { task: actualTaskType, model_cls: currentForm.model_cls, stage: currentForm.stage, prompt: currentForm.prompt.trim(), seed: currentForm.seed || Math.floor(Math.random() * 1000000) }; if (currentForm.model_cls.startsWith('wan2.1')) { formData.negative_prompt = "镜头晃动,色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" } if (selectedTaskId.value === 'i2v' && currentForm.imageFile) { const base64 = await fileToBase64(currentForm.imageFile); formData.input_image = { type: 'base64', data: base64 }; } if (selectedTaskId.value === 's2v') { if (currentForm.imageFile) { const base64 = await fileToBase64(currentForm.imageFile); formData.input_image = { type: 'base64', data: base64 }; } if (currentForm.audioFile) { const base64 = await fileToBase64(currentForm.audioFile); formData.input_audio = { type: 'base64', data: base64 }; formData.negative_prompt = "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" } } const response = await apiRequest('/api/v1/task/submit', { method: 'POST', body: JSON.stringify(formData) }); if (response && response.ok) { const result = await response.json(); showAlert(t('taskSubmitSuccessAlert'), 'success'); // 开始轮询新提交的任务状态 startPollingTask(result.task_id); // 保存完整的任务历史(包括提示词、图片和音频) await addTaskToHistory(selectedTaskId.value, currentForm); resetForm(selectedTaskId.value); // 重置当前任务类型的表单(保留模型选择,清空图片、音频和提示词) selectedTaskId.value = selectedTaskId.value; selectModel(currentForm.model_cls); switchToProjectsView(true); } else { const error = await response.json(); showAlert(`${t('taskSubmitFailedAlert')}: ${error.message},${error.detail}`, 'danger'); } } catch (error) { showAlert(`${t('submitTaskFailedAlert')}: ${error.message}`, 'danger'); } finally { submitting.value = false; } }; const fileToBase64 = (file) => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => { const base64 = reader.result.split(',')[1]; resolve(base64); }; reader.onerror = error => reject(error); }); }; const formatTime = (timestamp) => { if (!timestamp) return ''; const date = new Date(timestamp * 1000); return date.toLocaleString('zh-CN'); }; // 通用缓存管理函数 const loadFromCache = (cacheKey, expiryKey) => { try { const cached = localStorage.getItem(cacheKey); if (cached) { const data = JSON.parse(cached); if (Date.now() - data.timestamp < expiryKey) { console.log(`成功从缓存加载数据${cacheKey}:`, data.data); return data.data; } else { // 缓存过期,清除 localStorage.removeItem(cacheKey); console.log(`缓存过期,清除 ${cacheKey}`); } } } catch (error) { console.warn(`加载缓存失败 ${cacheKey}:`, error); localStorage.removeItem(cacheKey); } return null; }; const saveToCache = (cacheKey, data) => { try { const cacheData = { data: data, timestamp: Date.now() }; console.log(`成功保存缓存数据 ${cacheKey}:`, cacheData); localStorage.setItem(cacheKey, JSON.stringify(cacheData)); } catch (error) { console.warn(`保存缓存失败 ${cacheKey}:`, error); } }; // 清除所有应用缓存 const clearAllCache = () => { try { const cacheKeys = [ TASK_FILE_CACHE_KEY, TEMPLATE_FILE_CACHE_KEY, MODELS_CACHE_KEY, TEMPLATES_CACHE_KEY ]; // 清除所有任务缓存(使用通配符匹配) for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith(TASKS_CACHE_KEY)) { localStorage.removeItem(key); } } // 清除所有模板缓存(使用通配符匹配) for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith(TEMPLATES_CACHE_KEY)) { localStorage.removeItem(key); } } // 清除其他缓存 cacheKeys.forEach(key => { localStorage.removeItem(key); }); // 清除内存中的任务文件缓存 taskFileCache.value.clear(); taskFileCacheLoaded.value = false; // 清除内存中的模板文件缓存 templateFileCache.value.clear(); templateFileCacheLoaded.value = false; console.log('所有缓存已清除'); } catch (error) { console.warn('清除缓存失败:', error); } }; // 模板文件缓存管理函数 const loadTemplateFilesFromCache = () => { try { const cached = localStorage.getItem(TEMPLATE_FILE_CACHE_KEY); if (cached) { const data = JSON.parse(cached); if (data.files) { for (const [cacheKey, fileData] of Object.entries(data.files)) { templateFileCache.value.set(cacheKey, fileData); } return true; } else { console.warn('模板文件缓存数据格式错误'); return false; } } } catch (error) { console.warn('加载模板文件缓存失败:', error); } return false; }; const saveTemplateFilesToCache = () => { try { const files = {}; for (const [cacheKey, fileData] of templateFileCache.value.entries()) { files[cacheKey] = fileData; } const data = { files: files, timestamp: Date.now() }; localStorage.setItem(TEMPLATE_FILE_CACHE_KEY, JSON.stringify(data)); } catch (error) { console.warn('保存模板文件缓存失败:', error); } }; const getTemplateFileCacheKey = (templateId, fileKey) => { return `template_${templateId}_${fileKey}`; }; const getTemplateFileFromCache = (cacheKey) => { return templateFileCache.value.get(cacheKey) || null; }; const setTemplateFileToCache = (fileKey, fileData) => { templateFileCache.value.set(fileKey, fileData); // 异步保存到localStorage setTimeout(() => { saveTemplateFilesToCache(); }, 100); }; const getTemplateFileUrlFromApi = async (fileKey, fileType) => { const apiUrl = `/api/v1/template/asset_url/${fileType}/${fileKey}`; const response = await apiRequest(apiUrl); if (response && response.ok) { const data = await response.json(); let assertUrl = data.url; if (assertUrl.startsWith('./assets/')) { const token = localStorage.getItem('accessToken'); if (token) { assertUrl = `${assertUrl}&token=${encodeURIComponent(token)}`; } } setTemplateFileToCache(fileKey, { url: assertUrl, timestamp: Date.now() }); return assertUrl; } return null; }; // 获取模板文件URL(优先从缓存,缓存没有则生成URL)- 同步版本 const getTemplateFileUrl = (fileKey, fileType) => { // 检查参数有效性 if (!fileKey) { console.warn('getTemplateFileUrl: fileKey为空', { fileKey, fileType }); return null; } // 先从缓存获取 const cachedFile = getTemplateFileFromCache(fileKey); if (cachedFile) { console.log('从缓存获取模板文件url', { fileKey}); return cachedFile.url; } // 如果缓存中没有,返回null,让调用方知道需要异步获取 console.warn('模板文件URL不在缓存中,需要异步获取:', { fileKey, fileType }); getTemplateFileUrlAsync(fileKey, fileType).then(url => { return url; }); return null; }; // 创建响应式的模板文件URL(用于首屏渲染) const createTemplateFileUrlRef = (fileKey, fileType) => { const urlRef = ref(null); // 检查参数有效性 if (!fileKey) { console.warn('createTemplateFileUrlRef: fileKey为空', { fileKey, fileType }); return urlRef; } // 先从缓存获取 const cachedFile = getTemplateFileFromCache(fileKey); if (cachedFile) { urlRef.value = cachedFile.url; return urlRef; } // 检查是否正在获取中,避免重复请求 const fetchKey = `${fileKey}_${fileType}`; if (templateUrlFetching.value.has(fetchKey)) { console.log('createTemplateFileUrlRef: 正在获取中,跳过重复请求', { fileKey, fileType }); return urlRef; } // 标记为正在获取 templateUrlFetching.value.add(fetchKey); // 如果缓存中没有,异步获取 getTemplateFileUrlFromApi(fileKey, fileType).then(url => { if (url) { urlRef.value = url; // 将获取到的URL存储到缓存中 setTemplateFileToCache(fileKey, { url, timestamp: Date.now() }); } }).catch(error => { console.warn('获取模板文件URL失败:', error); }).finally(() => { // 移除获取状态 templateUrlFetching.value.delete(fetchKey); }); return urlRef; }; // 创建响应式的任务文件URL(用于首屏渲染) const createTaskFileUrlRef = (taskId, fileKey) => { const urlRef = ref(null); // 检查参数有效性 if (!taskId || !fileKey) { console.warn('createTaskFileUrlRef: 参数为空', { taskId, fileKey }); return urlRef; } // 先从缓存获取 const cachedFile = getTaskFileFromCache(taskId, fileKey); if (cachedFile) { urlRef.value = cachedFile.url; return urlRef; } // 如果缓存中没有,异步获取 getTaskFileUrl(taskId, fileKey).then(url => { if (url) { urlRef.value = url; // 将获取到的URL存储到缓存中 setTaskFileToCache(taskId, fileKey, { url, timestamp: Date.now() }); } }).catch(error => { console.warn('获取任务文件URL失败:', error); }); return urlRef; }; // 获取模板文件URL(异步版本,用于预加载等场景) const getTemplateFileUrlAsync = async (fileKey, fileType) => { // 检查参数有效性 if (!fileKey) { console.warn('getTemplateFileUrlAsync: fileKey为空', { fileKey, fileType }); return null; } // 先从缓存获取 const cachedFile = getTemplateFileFromCache(fileKey); if (cachedFile) { console.log('getTemplateFileUrlAsync: 从缓存获取', { fileKey, url: cachedFile.url }); return cachedFile.url; } // 检查是否正在获取中,避免重复请求 const fetchKey = `${fileKey}_${fileType}`; if (templateUrlFetching.value.has(fetchKey)) { console.log('getTemplateFileUrlAsync: 正在获取中,等待完成', { fileKey, fileType }); // 等待其他请求完成 return new Promise((resolve) => { const checkInterval = setInterval(() => { const cachedFile = getTemplateFileFromCache(fileKey); if (cachedFile) { clearInterval(checkInterval); resolve(cachedFile.url); } else if (!templateUrlFetching.value.has(fetchKey)) { clearInterval(checkInterval); resolve(null); } }, 100); }); } // 标记为正在获取 templateUrlFetching.value.add(fetchKey); // 如果缓存中没有,异步获取 try { const url = await getTemplateFileUrlFromApi(fileKey, fileType); if (url) { // 将获取到的URL存储到缓存中 setTemplateFileToCache(fileKey, { url, timestamp: Date.now() }); } return url; } catch (error) { console.warn('getTemplateFileUrlAsync: 获取URL失败', error); return null; } finally { // 移除获取状态 templateUrlFetching.value.delete(fetchKey); } }; // 任务文件缓存管理函数 const loadTaskFilesFromCache = () => { try { const cached = localStorage.getItem(TASK_FILE_CACHE_KEY); if (cached) { const data = JSON.parse(cached); // 检查是否过期 if (Date.now() - data.timestamp < TASK_FILE_CACHE_EXPIRY) { // 将缓存数据加载到内存缓存中 for (const [cacheKey, fileData] of Object.entries(data.files)) { taskFileCache.value.set(cacheKey, fileData); } return true; } else { // 缓存过期,清除 localStorage.removeItem(TASK_FILE_CACHE_KEY); } } } catch (error) { console.warn('加载任务文件缓存失败:', error); localStorage.removeItem(TASK_FILE_CACHE_KEY); } return false; }; const saveTaskFilesToCache = () => { try { const files = {}; for (const [cacheKey, fileData] of taskFileCache.value.entries()) { files[cacheKey] = fileData; } const data = { files, timestamp: Date.now() }; localStorage.setItem(TASK_FILE_CACHE_KEY, JSON.stringify(data)); } catch (error) { console.warn('保存任务文件缓存失败:', error); } }; // 生成缓存键 const getTaskFileCacheKey = (taskId, fileKey) => { return `${taskId}_${fileKey}`; }; // 从缓存获取任务文件 const getTaskFileFromCache = (taskId, fileKey) => { const cacheKey = getTaskFileCacheKey(taskId, fileKey); return taskFileCache.value.get(cacheKey) || null; }; // 设置任务文件到缓存 const setTaskFileToCache = (taskId, fileKey, fileData) => { const cacheKey = getTaskFileCacheKey(taskId, fileKey); taskFileCache.value.set(cacheKey, fileData); // 异步保存到localStorage setTimeout(() => { saveTaskFilesToCache(); }, 100); }; const getTaskFileUrlFromApi = async (taskId, fileKey) => { let apiUrl = `/api/v1/task/input_url?task_id=${taskId}&name=${fileKey}`; if (fileKey.includes('output')) { apiUrl = `/api/v1/task/result_url?task_id=${taskId}&name=${fileKey}`; } const response = await apiRequest(apiUrl); if (response && response.ok) { const data = await response.json(); let assertUrl = data.url; if (assertUrl.startsWith('./assets/')) { const token = localStorage.getItem('accessToken'); if (token) { assertUrl = `${assertUrl}&token=${encodeURIComponent(token)}`; } } setTaskFileToCache(taskId, fileKey, { url: assertUrl, timestamp: Date.now() }); return assertUrl; } return null; }; // 获取任务文件URL(优先从缓存,缓存没有则调用后端) const getTaskFileUrl = async (taskId, fileKey) => { // 先从缓存获取 const cachedFile = getTaskFileFromCache(taskId, fileKey); if (cachedFile) { return cachedFile.url; } return await getTaskFileUrlFromApi(taskId, fileKey); }; // 同步获取任务文件URL(仅从缓存获取,用于模板显示) const getTaskFileUrlSync = (taskId, fileKey) => { const cachedFile = getTaskFileFromCache(taskId, fileKey); if (cachedFile) { console.log('getTaskFileUrlSync: 从缓存获取', { taskId, fileKey, url: cachedFile.url, type: typeof cachedFile.url }); return cachedFile.url; } console.log('getTaskFileUrlSync: 缓存中没有找到', { taskId, fileKey }); return null; }; // 预加载任务文件 const preloadTaskFilesUrl = async (tasks) => { if (!tasks || tasks.length === 0) return; // 先尝试从localStorage加载缓存 if (taskFileCache.value.size === 0) { loadTaskFilesFromCache(); } console.log(`开始获取 ${tasks.length} 个任务的文件url`); // 分批预加载,避免过多并发请求 const batchSize = 5; for (let i = 0; i < tasks.length; i += batchSize) { const batch = tasks.slice(i, i + batchSize); const promises = batch.map(async (task) => { if (!task.task_id) return; // 预加载输入图片 if (task.inputs && task.inputs.input_image) { await getTaskFileUrl(task.task_id, 'input_image'); } // 预加载输入音频 if (task.inputs && task.inputs.input_audio) { await getTaskFileUrl(task.task_id, 'input_audio'); } // 预加载输出视频 if (task.outputs && task.outputs.output_video && task.status === 'SUCCEED') { await getTaskFileUrl(task.task_id, 'output_video'); } }); await Promise.all(promises); // 批次间添加延迟 if (i + batchSize < tasks.length) { await new Promise(resolve => setTimeout(resolve, 200)); } } console.log('任务文件url预加载完成'); }; // 预加载模板文件 const preloadTemplateFilesUrl = async (templates) => { if (!templates || templates.length === 0) return; // 先尝试从localStorage加载缓存 if (templateFileCache.value.size === 0) { loadTemplateFilesFromCache(); } console.log(`开始获取 ${templates.length} 个模板的文件url`); // 分批预加载,避免过多并发请求 const batchSize = 5; for (let i = 0; i < templates.length; i += batchSize) { const batch = templates.slice(i, i + batchSize); const promises = batch.map(async (template) => { if (!template.task_id) return; // 预加载视频文件 if (template.outputs?.output_video) { await getTemplateFileUrlAsync(template.outputs.output_video, 'videos'); } // 预加载图片文件 if (template.inputs?.input_image) { await getTemplateFileUrlAsync(template.inputs.input_image, 'images'); } // 预加载音频文件 if (template.inputs?.input_audio) { await getTemplateFileUrlAsync(template.inputs.input_audio, 'audios'); } }); await Promise.all(promises); // 批次间添加延迟 if (i + batchSize < templates.length) { await new Promise(resolve => setTimeout(resolve, 200)); } } console.log('模板文件url预加载完成'); }; const refreshTasks = async (forceRefresh = false) => { try { console.log('开始刷新任务列表, forceRefresh:', forceRefresh, 'currentPage:', currentTaskPage.value); // 构建缓存键,包含分页和过滤条件 const cacheKey = `${TASKS_CACHE_KEY}_${currentTaskPage.value}_${taskPageSize.value}_${statusFilter.value}_${taskSearchQuery.value}`; // 如果不是强制刷新,先尝试从缓存加载 if (!forceRefresh) { const cachedTasks = loadFromCache(cacheKey, TASKS_CACHE_EXPIRY); if (cachedTasks) { console.log('从缓存加载任务列表'); tasks.value = cachedTasks.tasks || []; pagination.value = cachedTasks.pagination || null; // 强制触发响应式更新 await nextTick(); // 强制刷新分页组件 paginationKey.value++; // 使用新的任务文件预加载逻辑 await preloadTaskFilesUrl(tasks.value); return; } } const params = new URLSearchParams({ page: currentTaskPage.value.toString(), page_size: taskPageSize.value.toString() }); if (statusFilter.value !== 'ALL') { params.append('status', statusFilter.value); } console.log('请求任务列表API:', `/api/v1/task/list?${params.toString()}`); const response = await apiRequest(`/api/v1/task/list?${params.toString()}`); if (response && response.ok) { const data = await response.json(); console.log('任务列表API响应:', data); // 强制清空并重新赋值,确保Vue检测到变化 tasks.value = []; pagination.value = null; await nextTick(); tasks.value = data.tasks || []; pagination.value = data.pagination || null; // 缓存任务数据 saveToCache(cacheKey, { tasks: data.tasks || [], pagination: data.pagination || null }); console.log('缓存任务列表数据成功'); // 强制触发响应式更新 await nextTick(); // 强制刷新分页组件 paginationKey.value++; // 使用新的任务文件预加载逻辑 await preloadTaskFilesUrl(tasks.value); } else if (response) { showAlert('刷新任务列表失败', 'danger'); } // 如果response为null,说明是认证错误,apiRequest已经处理了 } catch (error) { console.error('刷新任务列表失败:', error); // showAlert(`刷新任务列表失败: ${error.message}`, 'danger'); } }; // 分页相关函数 const goToPage = async (page) => { isLoading.value = true; if (page < 1 || page > pagination.value?.total_pages || page === currentTaskPage.value) { isLoading.value = false; return; } currentTaskPage.value = page; taskPageInput.value = page; // 同步更新输入框 await refreshTasks(); isLoading.value = false; }; const jumpToPage = async () => { const page = parseInt(taskPageInput.value); if (page && page >= 1 && page <= pagination.value?.total_pages && page !== currentTaskPage.value) { await goToPage(page); } else { // 如果输入无效,恢复到当前页 taskPageInput.value = currentTaskPage.value; } }; // Template分页相关函数 const goToTemplatePage = async (page) => { isLoading.value=true; if (page < 1 || page > templatePagination.value?.total_pages || page === templateCurrentPage.value) { isLoading.value = false; return; } templateCurrentPage.value = page; templatePageInput.value = page; // 同步更新输入框 await loadImageAudioTemplates(); isLoading.value = false; }; const jumpToTemplatePage = async () => { const page = parseInt(templatePageInput.value); if (page && page >= 1 && page <= templatePagination.value?.total_pages && page !== templateCurrentPage.value) { await goToTemplatePage(page); } else { // 如果输入无效,恢复到当前页 templatePageInput.value = templateCurrentPage.value; } }; const getVisiblePages = () => { if (!pagination.value) return []; const totalPages = pagination.value.total_pages; const current = currentTaskPage.value; const pages = []; // 总是显示第一页 pages.push(1); if (totalPages <= 5) { // 如果总页数少于等于7页,显示所有页码 for (let i = 2; i <= totalPages - 1; i++) { pages.push(i); } } else { // 如果总页数大于7页,使用省略号 if (current <= 3) { // 当前页在前4页 for (let i = 2; i <= 3; i++) { pages.push(i); } pages.push('...'); } else if (current >= totalPages - 2) { // 当前页在后4页 pages.push('...'); for (let i = totalPages - 2; i <= totalPages - 1; i++) { pages.push(i); } } else { // 当前页在中间 pages.push('...'); for (let i = current - 1; i <= current + 1; i++) { pages.push(i); } pages.push('...'); } } // 总是显示最后一页(如果不是第一页) if (totalPages > 1) { pages.push(totalPages); } return pages; }; const getVisibleTemplatePages = () => { if (!templatePagination.value) return []; const totalPages = templatePagination.value.total_pages; const current = templateCurrentPage.value; const pages = []; // 总是显示第一页 pages.push(1); if (totalPages <= 5) { // 如果总页数少于等于7页,显示所有页码 for (let i = 2; i <= totalPages - 1; i++) { pages.push(i); } } else { // 显示当前页附近的页码 const start = Math.max(2, current - 1); const end = Math.min(totalPages - 1, current + 1); if (start > 2) { pages.push('...'); } for (let i = start; i <= end; i++) { if (i !== 1 && i !== totalPages) { pages.push(i); } } if (end < totalPages - 1) { pages.push('...'); } } // 总是显示最后一页 if (totalPages > 1) { pages.push(totalPages); } return pages; }; // 灵感广场分页相关函数 const goToInspirationPage = async (page) => { isLoading.value = true; if (page < 1 || page > inspirationPagination.value?.total_pages || page === inspirationCurrentPage.value) { isLoading.value = false; return; } inspirationCurrentPage.value = page; inspirationPageInput.value = page; // 同步更新输入框 await loadInspirationData(); isLoading.value = false; }; const jumpToInspirationPage = async () => { const page = parseInt(inspirationPageInput.value); if (page && page >= 1 && page <= inspirationPagination.value?.total_pages && page !== inspirationCurrentPage.value) { await goToInspirationPage(page); } else { // 如果输入无效,恢复到当前页 inspirationPageInput.value = inspirationCurrentPage.value; } }; const getVisibleInspirationPages = () => { if (!inspirationPagination.value) return []; const totalPages = inspirationPagination.value.total_pages; const current = inspirationCurrentPage.value; const pages = []; // 总是显示第一页 pages.push(1); if (totalPages <= 5) { // 如果总页数少于等于7页,显示所有页码 for (let i = 2; i <= totalPages - 1; i++) { pages.push(i); } } else { // 显示当前页附近的页码 const start = Math.max(2, current - 1); const end = Math.min(totalPages - 1, current + 1); if (start > 2) { pages.push('...'); } for (let i = start; i <= end; i++) { if (i !== 1 && i !== totalPages) { pages.push(i); } } if (end < totalPages - 1) { pages.push('...'); } } // 总是显示最后一页 if (totalPages > 1) { pages.push(totalPages); } return pages; }; const getStatusBadgeClass = (status) => { const statusMap = { 'SUCCEED': 'bg-success', 'FAILED': 'bg-danger', 'RUNNING': 'bg-warning', 'PENDING': 'bg-secondary', 'CREATED': 'bg-secondary' }; return statusMap[status] || 'bg-secondary'; }; const viewSingleResult = async (taskId, key) => { try { downloadLoading.value = true; const url = await getTaskFileUrl(taskId, key); if (url) { const response = await fetch(url); if (response.ok) { const blob = await response.blob(); const videoBlob = new Blob([blob], { type: 'video/mp4' }); const url = window.URL.createObjectURL(videoBlob); window.open(url, '_blank'); } else { showAlert('获取结果失败', 'danger'); } } else { showAlert(t('getTaskResultFailedAlert'), 'danger'); } } catch (error) { showAlert(`${t('viewTaskResultFailedAlert')}: ${error.message}`, 'danger'); } finally { downloadLoading.value = false; } }; const cancelTask = async (taskId, fromDetailPage = false) => { try { // 显示确认对话框 const confirmed = await showConfirmDialog({ title: t('cancelTaskConfirm'), message: t('cancelTaskConfirmMessage'), confirmText: t('confirmCancel'), }); if (!confirmed) { return; } const response = await apiRequest(`/api/v1/task/cancel?task_id=${taskId}`); if (response && response.ok) { showAlert(t('taskCancelSuccessAlert'), 'success'); // 如果当前在任务详情界面,先刷新任务列表,然后重新获取任务信息 if (fromDetailPage) { refreshTasks(true); // 强制刷新 const updatedTask = tasks.value.find(t => t.task_id === taskId); if (updatedTask) { selectedTask.value = updatedTask; } await nextTick(); } else { refreshTasks(true); // 强制刷新 } } else if (response) { const error = await response.json(); showAlert(`${t('cancelTaskFailedAlert')}: ${error.message}`, 'danger'); } // 如果response为null,说明是认证错误,apiRequest已经处理了 } catch (error) { showAlert(`${t('cancelTaskFailedAlert')}: ${error.message}`, 'danger'); } }; const resumeTask = async (taskId, fromDetailPage = false) => { try { // 先获取任务信息,检查任务状态 const taskResponse = await apiRequest(`/api/v1/task/query?task_id=${taskId}`); if (!taskResponse || !taskResponse.ok) { showAlert(t('taskNotFoundAlert'), 'danger'); return; } const task = await taskResponse.json(); // 如果任务已完成,则删除并重新生成 if (task.status === 'SUCCEED') { // 显示确认对话框 const confirmed = await showConfirmDialog({ title: t('regenerateTaskConfirm'), message: t('regenerateTaskConfirmMessage'), confirmText: t('confirmRegenerate') }); if (!confirmed) { return; } // 显示重新生成中的提示 showAlert(t('regeneratingTaskAlert'), 'info'); const deleteResponse = await apiRequest(`/api/v1/task/delete?task_id=${taskId}`, { method: 'DELETE' }); if (!deleteResponse || !deleteResponse.ok) { showAlert(t('deleteTaskFailedAlert'), 'danger'); return; } try { // 设置任务类型 selectedTaskId.value = task.task_type; console.log('selectedTaskId.value', selectedTaskId.value); // 获取当前表单 const currentForm = getCurrentForm(); // 设置模型 if (task.params && task.params.model_cls) { currentForm.model_cls = task.params.model_cls; } // 设置prompt if (task.params && task.params.prompt) { currentForm.prompt = task.params.prompt; } // 尝试从localStorage获取任务历史数据 const taskHistory = JSON.parse(localStorage.getItem('taskHistory') || '[]'); const historyItem = taskHistory.find(item => item.task_id === task.task_id); if (historyItem) { // 从localStorage恢复图片和音频 if (historyItem.imageFile && historyItem.imageFile.blob) { // 重新创建File对象 const imageFile = new File([historyItem.imageFile.blob], historyItem.imageFile.name, { type: historyItem.imageFile.type }); currentForm.imageFile = imageFile; setCurrentImagePreview(URL.createObjectURL(imageFile)); } if (historyItem.audioFile && historyItem.audioFile.blob) { // 重新创建File对象 let mimeType = historyItem.audioFile.type; if (!mimeType || mimeType === 'application/octet-stream') { const filename = historyItem.audioFile.name || 'audio.wav'; const ext = filename.toLowerCase().split('.').pop(); const mimeTypes = { 'mp3': 'audio/mpeg', 'wav': 'audio/wav', 'mp4': 'audio/mp4', 'aac': 'audio/aac', 'ogg': 'audio/ogg', 'm4a': 'audio/mp4', 'webm': 'audio/webm' }; mimeType = mimeTypes[ext] || 'audio/mpeg'; } const audioFile = new File([historyItem.audioFile.blob], historyItem.audioFile.name, { type: mimeType }); currentForm.audioFile = audioFile; console.log('复用任务 - 从localStorage恢复音频文件:', { name: audioFile.name, type: audioFile.type, size: audioFile.size }); // 使用FileReader生成data URL,与正常上传保持一致 const reader = new FileReader(); reader.onload = (e) => { setCurrentAudioPreview(e.target.result); console.log('复用任务 - 音频预览已设置:', e.target.result.substring(0, 50) + '...'); }; reader.readAsDataURL(audioFile); } } else { // 如果localStorage中没有,尝试从后端获取任务文件 try { // 使用现有的函数获取图片和音频URL const imageUrl = await getTaskInputImage(task); const audioUrl = await getTaskInputAudio(task); // 加载图片文件 if (imageUrl) { try { const imageResponse = await fetch(imageUrl); if (imageResponse && imageResponse.ok) { const blob = await imageResponse.blob(); const filename = task.inputs[Object.keys(task.inputs).find(key => key.includes('image') || task.inputs[key].toString().toLowerCase().match(/\.(jpg|jpeg|png|gif|bmp|webp)$/) )] || 'image.jpg'; const file = new File([blob], filename, { type: blob.type }); currentForm.imageFile = file; setCurrentImagePreview(URL.createObjectURL(file)); } } catch (error) { console.warn('Failed to load image file:', error); } } // 加载音频文件 if (audioUrl) { try { const audioResponse = await fetch(audioUrl); if (audioResponse && audioResponse.ok) { const blob = await audioResponse.blob(); const filename = task.inputs[Object.keys(task.inputs).find(key => key.includes('audio') || task.inputs[key].toString().toLowerCase().match(/\.(mp3|wav|mp4|aac|ogg|m4a)$/) )] || 'audio.wav'; // 根据文件扩展名确定正确的MIME类型 let mimeType = blob.type; if (!mimeType || mimeType === 'application/octet-stream') { const ext = filename.toLowerCase().split('.').pop(); const mimeTypes = { 'mp3': 'audio/mpeg', 'wav': 'audio/wav', 'mp4': 'audio/mp4', 'aac': 'audio/aac', 'ogg': 'audio/ogg', 'm4a': 'audio/mp4' }; mimeType = mimeTypes[ext] || 'audio/mpeg'; } const file = new File([blob], filename, { type: mimeType }); currentForm.audioFile = file; console.log('复用任务 - 从后端加载音频文件:', { name: file.name, type: file.type, size: file.size, originalBlobType: blob.type }); // 使用FileReader生成data URL,与正常上传保持一致 const reader = new FileReader(); reader.onload = (e) => { setCurrentAudioPreview(e.target.result); console.log('复用任务 - 音频预览已设置:', e.target.result.substring(0, 50) + '...'); }; reader.readAsDataURL(file); } } catch (error) { console.warn('Failed to load audio file:', error); } } } catch (error) { console.warn('Failed to load task data from backend:', error); } } showAlert(t('taskMaterialReuseSuccessAlert'), 'success'); } catch (error) { console.error('Failed to resume task:', error); showAlert(t('loadTaskDataFailedAlert'), 'danger'); return; } // 如果从详情页调用,关闭详情页 if (fromDetailPage) { closeTaskDetailModal(); } submitTask(); return; // 不需要继续执行后续的API调用 } else { // 对于未完成的任务,使用原有的恢复逻辑 const response = await apiRequest(`/api/v1/task/resume?task_id=${taskId}`); if (response && response.ok) { showAlert(t('taskRetrySuccessAlert'), 'success'); // 如果当前在任务详情界面,先刷新任务列表,然后重新获取任务信息 if (fromDetailPage) { refreshTasks(true); // 强制刷新 const updatedTask = tasks.value.find(t => t.task_id === taskId); if (updatedTask) { selectedTask.value = updatedTask; } startPollingTask(taskId); await nextTick(); } else { refreshTasks(true); // 强制刷新 // 开始轮询新提交的任务状态 startPollingTask(taskId); } } else if (response) { const error = await response.json(); showAlert(`${t('retryTaskFailedAlert')}: ${error.message}`, 'danger'); } } } catch (error) { console.error('resumeTask error:', error); showAlert(`${t('retryTaskFailedAlert')}: ${error.message}`, 'danger'); } }; // 切换任务菜单显示状态 const toggleTaskMenu = (taskId) => { // 先关闭所有其他菜单 closeAllTaskMenus(); // 然后打开当前菜单 taskMenuVisible.value[taskId] = true; }; // 关闭所有任务菜单 const closeAllTaskMenus = () => { taskMenuVisible.value = {}; }; // 点击外部关闭菜单 const handleClickOutside = (event) => { if (!event.target.closest('.task-menu-container')) { closeAllTaskMenus(); } if (!event.target.closest('.task-type-dropdown')) { showTaskTypeMenu.value = false; } if (!event.target.closest('.model-dropdown')) { showModelMenu.value = false; } }; const deleteTask = async (taskId, fromDetailPage = false) => { try { // 显示确认对话框 const confirmed = await showConfirmDialog({ title: t('deleteTaskConfirm'), message: t('deleteTaskConfirmMessage'), confirmText: t('confirmDelete') }); if (!confirmed) { return; } // 显示删除中的提示 showAlert(t('deletingTaskAlert'), 'info'); const response = await apiRequest(`/api/v1/task/delete?task_id=${taskId}`, { method: 'DELETE' }); if (response && response.ok) { showAlert(t('taskDeletedSuccessAlert'), 'success'); refreshTasks(true); // 强制刷新 // 如果是从任务详情页删除,则跳转回主页 if (fromDetailPage) { if (!selectedTaskId.value) { if (availableTaskTypes.value.includes('s2v')) { selectTask('s2v'); } } } } else if (response) { const error = await response.json(); showAlert(`${t('deleteTaskFailedAlert')}: ${error.message}`, 'danger'); } // 如果response为null,说明是认证错误,apiRequest已经处理了 } catch (error) { showAlert(`${t('deleteTaskFailedAlert')}: ${error.message}`, 'danger'); } }; const loadTaskFiles = async (task) => { try { loadingTaskFiles.value = true; const files = { inputs: {}, outputs: {} }; // 获取输入文件(所有状态的任务都需要) if (task.inputs) { for (const [key, inputPath] of Object.entries(task.inputs)) { try { const url = await getTaskFileUrl(taskId, key); if (url) { const response = await fetch(url); if (response && response.ok) { const blob = await response.blob() files.inputs[key] = { name: inputPath, // 使用原始文件名而不是key path: inputPath, blob: blob, url: URL.createObjectURL(blob) } } } } catch (error) { console.error(`Failed to load input ${key}:`, error); files.inputs[key] = { name: inputPath, // 使用原始文件名而不是key path: inputPath, error: true }; } } } // 只对成功完成的任务获取输出文件 if (task.status === 'SUCCEED' && task.outputs) { for (const [key, outputPath] of Object.entries(task.outputs)) { try { const url = await getTaskFileUrl(taskId, key); if (url) { const response = await fetch(url); if (response && response.ok) { const blob = await response.blob() files.outputs[key] = { name: outputPath, // 使用原始文件名而不是key path: outputPath, blob: blob, url: URL.createObjectURL(blob) } }; } } catch (error) { console.error(`Failed to load output ${key}:`, error); files.outputs[key] = { name: outputPath, // 使用原始文件名而不是key path: outputPath, error: true }; } } } selectedTaskFiles.value = files; } catch (error) { console.error('Failed to load task files: task_id=', taskId, error); showAlert(t('loadTaskFilesFailedAlert'), 'danger'); } finally { loadingTaskFiles.value = false; } }; const reuseTask = async (task) => { try { // 跳转到任务创建界面 isCreationAreaExpanded.value=true if (showTaskDetailModal.value) { closeTaskDetailModal(); } // 设置任务类型 selectedTaskId.value = task.task_type; console.log('selectedTaskId.value', selectedTaskId.value); // 获取当前表单 const currentForm = getCurrentForm(); // 设置模型 if (task.params && task.params.model_cls) { currentForm.model_cls = task.params.model_cls; } // 设置prompt if (task.params && task.params.prompt) { currentForm.prompt = task.params.prompt; } // 尝试从localStorage获取任务历史数据 const taskHistory = JSON.parse(localStorage.getItem('taskHistory') || '[]'); const historyItem = taskHistory.find(item => item.task_id === task.task_id); if (historyItem) { // 从localStorage恢复图片和音频 if (historyItem.imageFile && historyItem.imageFile.blob) { // 重新创建File对象 const imageFile = new File([historyItem.imageFile.blob], historyItem.imageFile.name, { type: historyItem.imageFile.type }); currentForm.imageFile = imageFile; setCurrentImagePreview(URL.createObjectURL(imageFile)); } if (historyItem.audioFile && historyItem.audioFile.blob) { // 重新创建File对象 let mimeType = historyItem.audioFile.type; if (!mimeType || mimeType === 'application/octet-stream') { const filename = historyItem.audioFile.name || 'audio.wav'; const ext = filename.toLowerCase().split('.').pop(); const mimeTypes = { 'mp3': 'audio/mpeg', 'wav': 'audio/wav', 'mp4': 'audio/mp4', 'aac': 'audio/aac', 'ogg': 'audio/ogg', 'm4a': 'audio/mp4', 'webm': 'audio/webm' }; mimeType = mimeTypes[ext] || 'audio/mpeg'; } const audioFile = new File([historyItem.audioFile.blob], historyItem.audioFile.name, { type: mimeType }); currentForm.audioFile = audioFile; console.log('复用任务 - 从localStorage恢复音频文件:', { name: audioFile.name, type: audioFile.type, size: audioFile.size }); // 使用FileReader生成data URL,与正常上传保持一致 const reader = new FileReader(); reader.onload = (e) => { setCurrentAudioPreview(e.target.result); console.log('复用任务 - 音频预览已设置:', e.target.result.substring(0, 50) + '...'); }; reader.readAsDataURL(audioFile); } } else { // 如果localStorage中没有,尝试从后端获取任务文件 try { // 使用现有的函数获取图片和音频URL const imageUrl = await getTaskInputImage(task); const audioUrl = await getTaskInputAudio(task); // 加载图片文件 if (imageUrl) { try { const imageResponse = await fetch(imageUrl); if (imageResponse && imageResponse.ok) { const blob = await imageResponse.blob(); const filename = task.inputs[Object.keys(task.inputs).find(key => key.includes('image') || task.inputs[key].toString().toLowerCase().match(/\.(jpg|jpeg|png|gif|bmp|webp)$/) )] || 'image.jpg'; const file = new File([blob], filename, { type: blob.type }); currentForm.imageFile = file; setCurrentImagePreview(URL.createObjectURL(file)); } } catch (error) { console.warn('Failed to load image file:', error); } } // 加载音频文件 if (audioUrl) { try { const audioResponse = await fetch(audioUrl); if (audioResponse && audioResponse.ok) { const blob = await audioResponse.blob(); const filename = task.inputs[Object.keys(task.inputs).find(key => key.includes('audio') || task.inputs[key].toString().toLowerCase().match(/\.(mp3|wav|mp4|aac|ogg|m4a)$/) )] || 'audio.wav'; // 根据文件扩展名确定正确的MIME类型 let mimeType = blob.type; if (!mimeType || mimeType === 'application/octet-stream') { const ext = filename.toLowerCase().split('.').pop(); const mimeTypes = { 'mp3': 'audio/mpeg', 'wav': 'audio/wav', 'mp4': 'audio/mp4', 'aac': 'audio/aac', 'ogg': 'audio/ogg', 'm4a': 'audio/mp4' }; mimeType = mimeTypes[ext] || 'audio/mpeg'; } const file = new File([blob], filename, { type: mimeType }); currentForm.audioFile = file; console.log('复用任务 - 从后端加载音频文件:', { name: file.name, type: file.type, size: file.size, originalBlobType: blob.type }); // 使用FileReader生成data URL,与正常上传保持一致 const reader = new FileReader(); reader.onload = (e) => { setCurrentAudioPreview(e.target.result); console.log('复用任务 - 音频预览已设置:', e.target.result.substring(0, 50) + '...'); }; reader.readAsDataURL(file); } } catch (error) { console.warn('Failed to load audio file:', error); } } } catch (error) { console.warn('Failed to load task data from backend:', error); } } showAlert(t('taskMaterialReuseSuccessAlert'), 'success'); switchToCreateView(); } catch (error) { console.error('Failed to reuse task:', error); showAlert(t('loadTaskDataFailedAlert'), 'danger'); } }; const downloadFile = (fileInfo) => { if (!fileInfo || !fileInfo.blob) { showAlert(t('fileUnavailableAlert'), 'danger'); return; } try { const url = URL.createObjectURL(fileInfo.blob); const a = document.createElement('a'); a.href = url; a.download = fileInfo.name || 'download'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } catch (error) { console.error('Download failed:', error); showAlert(t('downloadFailedAlert'), 'danger'); } }; const viewFile = (fileInfo) => { if (!fileInfo || !fileInfo.url) { showAlert(t('fileUnavailableAlert'), 'danger'); return; } // 在新窗口中打开文件 window.open(fileInfo.url, '_blank'); }; const clearTaskFiles = () => { // 清理 URL 对象,释放内存 Object.values(selectedTaskFiles.value.inputs).forEach(file => { if (file.url) { URL.revokeObjectURL(file.url); } }); Object.values(selectedTaskFiles.value.outputs).forEach(file => { if (file.url) { URL.revokeObjectURL(file.url); } }); selectedTaskFiles.value = { inputs: {}, outputs: {} }; }; const showTaskCreator = () => { selectedTask.value = null; // clearTaskFiles(); // 清空文件缓存 selectedTaskId.value = 's2v'; // 默认选择数字人任务 // 停止所有任务状态轮询 pollingTasks.value.clear(); if (pollingInterval.value) { clearInterval(pollingInterval.value); pollingInterval.value = null; } }; const toggleSidebar = () => { sidebarCollapsed.value = !sidebarCollapsed.value; if (sidebarCollapsed.value) { // 收起时,将历史任务栏隐藏到屏幕左侧 if (sidebar.value) { sidebar.value.style.transform = 'translateX(-100%)'; } } else { // 展开时,恢复历史任务栏位置 if (sidebar.value) { sidebar.value.style.transform = 'translateX(0)'; } } // 更新悬浮按钮位置 updateFloatingButtonPosition(sidebarWidth.value); }; const clearPrompt = () => { getCurrentForm().prompt = ''; updateUploadedContentStatus(); }; const getTaskItemClass = (status) => { if (status === 'SUCCEED') return 'bg-laser-purple/15 border border-laser-purple/30'; if (status === 'RUNNING') return 'bg-laser-purple/15 border border-laser-purple/30'; if (status === 'FAILED') return 'bg-red-500/15 border border-red-500/30'; return 'bg-dark-light border border-gray-700'; }; const getStatusIndicatorClass = (status) => { const base = 'inline-block w-2 aspect-square rounded-full shrink-0 align-middle'; if (status === 'SUCCEED') return `${base} bg-gradient-to-r from-emerald-200 to-green-300 shadow-md shadow-emerald-300/30`; if (status === 'RUNNING') return `${base} bg-gradient-to-r from-amber-200 to-yellow-300 shadow-md shadow-amber-300/30 animate-pulse`; if (status === 'FAILED') return `${base} bg-gradient-to-r from-red-200 to-pink-300 shadow-md shadow-red-300/30`; return `${base} bg-gradient-to-r from-gray-200 to-gray-300 shadow-md shadow-gray-300/30`; }; const getTaskTypeBtnClass = (taskType) => { if (selectedTaskId.value === taskType) { return 'text-gradient-icon border-b-2 border-laser-purple'; } return 'text-gray-400 hover:text-gradient-icon'; }; const getModelBtnClass = (model) => { if (getCurrentForm().model_cls === model) { return 'bg-laser-purple/20 border border-laser-purple/40 active shadow-laser'; } return 'bg-dark-light border border-gray-700 hover:bg-laser-purple/15 hover:border-laser-purple/40 transition-all hover:shadow-laser'; }; const getTaskTypeIcon = (taskType) => { const iconMap = { 't2v': 'fas fa-font', 'i2v': 'fas fa-image', 's2v': 'fas fa-user' }; return iconMap[taskType] || 'fas fa-video'; }; const getTaskTypeName = (task) => { // 如果传入的是字符串,直接返回映射 if (!task) { return '未知'; } if (typeof task === 'string') { return nameMap.value[task] || task; } // 如果传入的是任务对象,根据模型类型判断 if (task && task.model_cls) { const modelCls = task.model_cls.toLowerCase(); return nameMap.value[task.task_type] || task.task_type; } // 默认返回task_type return task.task_type || '未知'; }; const getPromptPlaceholder = () => { if (selectedTaskId.value === 't2v') { return t('pleaseEnterThePromptForVideoGeneration') + ','+ t('describeTheContentStyleSceneOfTheVideo'); } else if (selectedTaskId.value === 'i2v') { return t('pleaseEnterThePromptForVideoGeneration') + ','+ t('describeTheContentActionRequirementsBasedOnTheImage'); } else if (selectedTaskId.value === 's2v') { return t('optional') + ' '+ t('pleaseEnterThePromptForVideoGeneration') + ','+ t('describeTheDigitalHumanImageBackgroundStyleActionRequirements'); } return t('pleaseEnterThePromptForVideoGeneration') + '...'; }; const getStatusTextClass = (status) => { if (status === 'SUCCEED') return 'text-emerald-400'; if (status === 'CREATED') return 'text-blue-400'; if (status === 'PENDING') return 'text-yellow-400'; if (status === 'RUNNING') return 'text-amber-400'; if (status === 'FAILED') return 'text-red-400'; if (status === 'CANCEL') return 'text-gray-400'; return 'text-gray-400'; }; const getImagePreview = (base64Data) => { if (!base64Data) return ''; return `data:image/jpeg;base64,${base64Data}`; }; const getTaskInputUrl = async (taskId, key) => { // 优先从缓存获取 const cachedUrl = getTaskFileUrlSync(taskId, key); if (cachedUrl) { console.log('getTaskInputUrl: 从缓存获取', { taskId, key, url: cachedUrl }); return cachedUrl; } return await getTaskFileUrlFromApi(taskId, key); }; const getTaskInputImage = async (task) => { if (!task || !task.inputs) { console.log('getTaskInputImage: 任务或输入为空', { task: task?.task_id, inputs: task?.inputs }); return null; } const imageInputs = Object.keys(task.inputs).filter(key => key.includes('image') || task.inputs[key].toString().toLowerCase().match(/\.(jpg|jpeg|png|gif|bmp|webp)$/) ); if (imageInputs.length > 0) { const firstImageKey = imageInputs[0]; // 优先从缓存获取 const cachedUrl = getTaskFileUrlSync(task.task_id, firstImageKey); if (cachedUrl) { console.log('getTaskInputImage: 从缓存获取', { taskId: task.task_id, key: firstImageKey, url: cachedUrl }); return cachedUrl; } // 缓存没有则生成URL const url = await getTaskInputUrl(task.task_id, firstImageKey); console.log('getTaskInputImage: 生成URL', { taskId: task.task_id, key: firstImageKey, url }); return url; } console.log('getTaskInputImage: 没有找到图片输入'); return null; }; const getTaskInputAudio = async (task) => { if (!task || !task.inputs) return null; const audioInputs = Object.keys(task.inputs).filter(key => key.includes('audio') || task.inputs[key].toString().toLowerCase().match(/\.(mp3|wav|mp4|aac|ogg|m4a)$/) ); if (audioInputs.length > 0) { const firstAudioKey = audioInputs[0]; return await getTaskInputUrl(task.task_id, firstAudioKey); } return null; }; const handleThumbnailError = (event) => { // 当输入图片加载失败时,显示默认图标 const img = event.target; const parent = img.parentElement; parent.innerHTML = '