From ad5735b6a5dedfcd0903f08af0eaf28c02d859bd Mon Sep 17 00:00:00 2001 From: iHeyTang Date: Thu, 11 Sep 2025 14:43:35 +0800 Subject: [PATCH] =?UTF-8?q?refactor(generate):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E7=94=9F=E6=88=90=E9=A1=B5=E9=9D=A2=E5=8F=8A=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E8=B5=84=E6=BA=90=EF=BC=8C=E4=BC=98=E5=8C=96=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除生成页面的配置、样式和逻辑文件,简化项目结构 - 更新历史和首页逻辑,调整跳转至结果页面的参数 - 增强结果页面的任务轮询逻辑,提升用户体验 - 添加生成中和错误状态的组件,优化代码可读性 --- src/app.config.ts | 1 - src/pages/generate/index.config.ts | 8 - src/pages/generate/index.css | 298 ------------------ src/pages/generate/index.tsx | 121 ------- src/pages/history/index.tsx | 5 +- src/pages/home/index.tsx | 2 +- .../result/components/ErrorComponent.tsx | 33 ++ .../result/components/GeneratingComponent.tsx | 101 ++++++ .../result/components/SuccessComponent.tsx | 155 +++++++++ src/pages/result/index.css | 243 ++++++++++++++ src/pages/result/index.tsx | 251 +++++---------- src/sdk/sdk-server.ts | 13 +- 12 files changed, 614 insertions(+), 617 deletions(-) delete mode 100644 src/pages/generate/index.config.ts delete mode 100644 src/pages/generate/index.css delete mode 100644 src/pages/generate/index.tsx create mode 100644 src/pages/result/components/ErrorComponent.tsx create mode 100644 src/pages/result/components/GeneratingComponent.tsx create mode 100644 src/pages/result/components/SuccessComponent.tsx diff --git a/src/app.config.ts b/src/app.config.ts index 1fb83e9..c6c725e 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -2,7 +2,6 @@ export default defineAppConfig({ pages: [ 'pages/home/index', // 新的模板卡片首页 'pages/history/index', // 历史记录页面 - 'pages/generate/index', // 生成处理页面 'pages/result/index', ], tabBar: { diff --git a/src/pages/generate/index.config.ts b/src/pages/generate/index.config.ts deleted file mode 100644 index 562ee1b..0000000 --- a/src/pages/generate/index.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -export default definePageConfig({ - navigationBarTitleText: '生成中', - navigationBarBackgroundColor: '#ffffff', - navigationBarTextStyle: 'black', - backgroundColorTop: '#ffffff', - backgroundColorBottom: '#ffffff', - backgroundColor: '#ffffff' -}) \ No newline at end of file diff --git a/src/pages/generate/index.css b/src/pages/generate/index.css deleted file mode 100644 index 9f5af08..0000000 --- a/src/pages/generate/index.css +++ /dev/null @@ -1,298 +0,0 @@ -.generate { - padding: 0; - min-height: 100vh; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%); -} - -.generate-loading { - text-align: center; - padding: 30px; -} - -.progress-container { - margin-bottom: 20px; -} - -.progress-bar { - width: 250px; - height: 6px; - background-color: #e9ecef; - border-radius: 3px; - overflow: hidden; - margin-bottom: 10px; -} - -.progress-fill { - height: 100%; - background: linear-gradient(90deg, #007aff 0%, #34c759 100%); - border-radius: 3px; - transition: width 0.3s ease; -} - -.progress-text { - font-size: 14px; - color: #666; - font-weight: 500; -} - -.loading-text { - font-size: 16px; - color: #333; - margin-top: 10px; -} - -.generate-success { - position: relative; - width: 100%; - height: 100vh; - display: flex; - flex-direction: column; - background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%); - padding: 20px 16px; - box-sizing: border-box; -} - -/* 成功页面头部 */ -.success-header { - text-align: center; - margin-bottom: 20px; - padding-top: 40px; -} - -.success-badge { - display: flex; - align-items: center; - justify-content: center; - gap: 6px; - margin-bottom: 12px; -} - -.success-icon { - font-size: 32px; - filter: drop-shadow(0 2px 4px rgb(0 0 0 / 10%)); -} - -.success-title { - font-size: 24px; - font-weight: 700; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - -webkit-text-fill-color: transparent; - background-clip: text; - letter-spacing: -0.25px; -} - -.success-subtitle { - font-size: 14px; - color: #666; - font-weight: 500; - line-height: 1.5; -} - -/* 预览区域 */ -.result-preview { - flex: 1; - display: flex; - justify-content: center; - align-items: center; - margin-bottom: 20px; - min-height: 0; - padding: 0 8px; -} - -.preview-container { - border-radius: 16px; - padding: 0; - box-shadow: 0 4px 16px rgb(0 0 0 / 10%); - border: 0.5px solid #f0f0f0; - position: relative; - overflow: hidden; - width: 90%; - max-width: 300px; - min-height: 340px; - height: 60vh; - display: block; -} - -.preview-image { - width: 100%; - height: 100%; - min-height: 340px; - object-fit: cover; - border-radius: 16px; - cursor: pointer; - display: block; - transform: translateZ(0); - image-rendering: optimizespeed; - backface-visibility: hidden; -} - -.preview-video { - width: 100%; - height: 100%; - min-height: 340px; - object-fit: cover; - border-radius: 16px; - display: block; - transform: translateZ(0); - backface-visibility: hidden; -} - -/* 底部操作区域 */ -.success-actions { - margin-top: auto; - width: 100%; -} - -.result-media { - width: 100%; - height: 100%; -} - -.result-image { - width: 100%; - height: 100%; -} - -.result-image image { - width: 100%; - height: 100%; -} - -.result-video { - width: 100%; - height: 100%; -} - -.generate-error { - text-align: center; - padding: 30px; - background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%); - min-height: 100vh; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} - -.error-container { - margin-bottom: 30px; - background: #fff; - border-radius: 16px; - padding: 30px 20px; - box-shadow: 0 4px 16px rgb(0 0 0 / 10%); - border: 0.5px solid #f0f0f0; - max-width: 300px; -} - -.error-text { - font-size: 20px; - color: #ff3b30; - font-weight: 700; - margin-bottom: 10px; - display: flex; - align-items: center; - justify-content: center; - gap: 6px; -} - -.error-text::before { - content: '⚠️'; - font-size: 18px; -} - -.error-message { - font-size: 15px; - color: #666; - line-height: 1.4; -} - -.action-buttons { - display: flex; - flex-direction: row; - gap: 12px; - width: 100%; - max-width: 300px; - margin: 0 auto 16px; -} - -.success-actions .action-buttons { - margin-bottom: 12px; -} - -/* 结果提示 */ -.result-tips { - text-align: center; - margin-top: 8px; -} - -.tips-text { - font-size: 12px; - color: #999; - background: rgb(255 255 255 / 80%); - padding: 6px 12px; - border-radius: 10px; - display: inline-block; - backdrop-filter: blur(5px); - box-shadow: 0 1px 4px rgb(0 0 0 / 5%); -} - -.btn-primary { - flex: 1; - background: linear-gradient(135deg, #ffd67a 0%, #ffc947 100%); - color: #333; - border-radius: 12px; - height: 48px; - font-size: 16px; - font-weight: 600; - border: none; - box-shadow: 0 4px 12px rgb(255 214 122 / 30%); - position: relative; - overflow: hidden; - display: flex; - align-items: center; - justify-content: center; -} - -.btn-primary::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, transparent, rgb(255 255 255 / 30%), transparent); - z-index: 1; -} - -.btn-primary::after { - border: none; -} - -.btn-secondary { - flex: 1; - background: rgb(255 255 255 / 90%); - color: #333; - border: 1px solid rgb(255 214 122 / 60%); - border-radius: 12px; - height: 48px; - font-size: 16px; - font-weight: 600; - box-shadow: 0 2px 8px rgb(0 0 0 / 8%); - backdrop-filter: blur(5px); - display: flex; - align-items: center; - justify-content: center; -} - -.btn-secondary::after { - border: none; -} - -.btn-text { - position: relative; - z-index: 2; -} diff --git a/src/pages/generate/index.tsx b/src/pages/generate/index.tsx deleted file mode 100644 index 5fc24b0..0000000 --- a/src/pages/generate/index.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { Button, Text, View } from '@tarojs/components'; -import Taro, { navigateTo, switchTab } from '@tarojs/taro'; -import { useEffect, useState } from 'react'; -import LoadingOverlay from '../../components/LoadingOverlay'; -import { useServerSdk } from '../../hooks/index'; -import './index.css'; - -export default function Generate() { - const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading'); - const [result, setResult] = useState(null); - const serverSdk = useServerSdk(); - let timerRef: NodeJS.Timeout | null = null; - - useEffect(() => { - const instance = Taro.getCurrentInstance(); - const params = instance.router?.params; - if (params?.taskId) { - pollTaskResult(params.taskId); - } else { - setStatus('error'); - setResult({ success: false, error: '缺少必要参数' }); - } - - return () => { - if (timerRef) { - clearTimeout(timerRef); - timerRef = null; - } - }; - }, []); - - const pollTaskResult = async (taskId: string) => { - try { - let attempts = 0; - const maxAttempts = 30 * 20; - - const poll = async () => { - attempts++; - - try { - const result = await serverSdk.getTaskProgress(taskId); - - if (result.status === 'completed') { - setStatus('success'); - navigateTo({ - url: `/pages/result/index?images=${encodeURIComponent(JSON.stringify([result.outputUrl]))}`, - }); - return; - } else if (result.status === 'failed') { - setStatus('error'); - setResult({ success: false, error: result.error || '处理失败' }); - } else if (attempts < maxAttempts) { - timerRef = setTimeout(poll, 1000); - } else { - setStatus('error'); - setResult({ success: false, error: '处理超时' }); - } - } catch (error) { - if (attempts < maxAttempts) { - timerRef = setTimeout(poll, 1000); - } else { - setStatus('error'); - setResult({ success: false, error: '获取结果失败' }); - } - } - }; - - poll(); - } catch (error) { - setStatus('error'); - setResult({ success: false, error: '开始处理失败' }); - } - }; - - const handleTryAgain = () => { - switchTab({ url: '/pages/home/index' }); - }; - - const handleViewHistory = () => { - switchTab({ url: '/pages/history/index' }); - }; - - const renderLoading = () => ( - - - - - - - - - ); - - const renderError = () => ( - - - 处理失败 - {result?.error || '未知错误'} - - - - - - - ); - - return ( - - {status === 'loading' && renderLoading()} - {status === 'error' && renderError()} - - ); -} diff --git a/src/pages/history/index.tsx b/src/pages/history/index.tsx index b2e5e41..bff7c7d 100644 --- a/src/pages/history/index.tsx +++ b/src/pages/history/index.tsx @@ -57,9 +57,8 @@ export default function History() { const handleItemClick = (record: any) => { if (record.status === 'completed' && record.outputUrl) { // 跳转到结果页面查看 - const images = Array.isArray(record.outputUrl) ? record.outputUrl : [record.outputUrl]; navigateTo({ - url: `/pages/result/index?images=${encodeURIComponent(JSON.stringify(images))}`, + url: `/pages/result/index?taskId=${record.executionId}`, }); } else if (record.status === 'failed') { // 显示错误信息 @@ -71,7 +70,7 @@ export default function History() { } else if (record.status === 'processing') { // 跳转到生成页面查看进度 navigateTo({ - url: `/pages/generate/index?taskId=${record.taskId}`, + url: `/pages/result/index?taskId=${record.executionId}`, }); } }; diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx index 8760182..7aa2736 100644 --- a/src/pages/home/index.tsx +++ b/src/pages/home/index.tsx @@ -144,7 +144,7 @@ export default function Home() { Taro.hideLoading(); // 跳转到生成页面 navigateTo({ - url: `/pages/generate/index?taskId=${taskId}&templateCode=${template.code}`, + url: `/pages/result/index?taskId=${taskId}&templateCode=${template.code}`, }); } catch (businessError) { // 业务处理失败 diff --git a/src/pages/result/components/ErrorComponent.tsx b/src/pages/result/components/ErrorComponent.tsx new file mode 100644 index 0000000..60c16bf --- /dev/null +++ b/src/pages/result/components/ErrorComponent.tsx @@ -0,0 +1,33 @@ +import { View } from '@tarojs/components'; +import { navigateBack, switchTab } from '@tarojs/taro'; + +interface ErrorComponentProps { + error: string; +} + +const ErrorComponent: React.FC = ({ error }) => { + const handleTryAgain = () => { + switchTab({ url: '/pages/home/index' }); + }; + + return ( + + + + 处理失败 + {error} + + + + 再来一张 + + navigateBack()}> + 返回 + + + + + ); +}; + +export default ErrorComponent; diff --git a/src/pages/result/components/GeneratingComponent.tsx b/src/pages/result/components/GeneratingComponent.tsx new file mode 100644 index 0000000..6df4a0b --- /dev/null +++ b/src/pages/result/components/GeneratingComponent.tsx @@ -0,0 +1,101 @@ +import { Image, View } from '@tarojs/components'; +import { navigateBack } from '@tarojs/taro'; +import { useEffect, useState } from 'react'; + +interface GeneratingComponentProps { + task: any; +} + +const GeneratingComponent: React.FC = ({ task }) => { + const [progress, setProgress] = useState(0); + + // 根据任务创建时间和类型计算真实进度 + useEffect(() => { + let interval: NodeJS.Timeout | null = null; + + const calculateProgress = () => { + if (!task?.createdAt) return 0; + + const createdAt = new Date(task.createdAt).getTime(); + const now = Date.now(); + const elapsedMinutes = (now - createdAt) / (1000 * 60); + + // 根据任务类型确定预计时间 + const isVideo = task.taskType === 'video' || task.outputType === 'video' || task.type === 'video'; + const estimatedTime = isVideo ? 2.5 : 1; // 视频3分钟,图片1分钟 + + // 计算进度百分比,最大95% + const calculatedProgress = Math.min((elapsedMinutes / estimatedTime) * 100, 95); + return Math.max(calculatedProgress, 0); + }; + + // 立即计算一次 + setProgress(calculateProgress()); + + // 每秒更新一次进度 + interval = setInterval(() => { + setProgress(calculateProgress()); + }, 1000); + + return () => { + if (interval) { + clearInterval(interval); + } + }; + }, [task]); + + const handleLaterView = () => { + navigateBack(); + }; + + return ( + + + {/* 进度方框 */} + + + + + {/* 预览图片或占位符 */} + {task?.inputImageUrl ? ( + + ) : ( + + 🎨 + + )} + {/* 光扫描动画 */} + + + + + + {/* 状态文本 */} + + 生成中... {Math.round(progress)}% + 内容生成中,请耐心等待 + + + {/* 操作按钮 */} + + + 稍后查看 + + + + {/* 提示信息 */} + + · T I P S · + 退出本页面不影响生成进度 + + + + ); +}; + +export default GeneratingComponent; diff --git a/src/pages/result/components/SuccessComponent.tsx b/src/pages/result/components/SuccessComponent.tsx new file mode 100644 index 0000000..8ba88df --- /dev/null +++ b/src/pages/result/components/SuccessComponent.tsx @@ -0,0 +1,155 @@ +import { Image, Video, View } from '@tarojs/components'; +import { downloadFile, hideToast, saveImageToPhotosAlbum, saveVideoToPhotosAlbum, showToast, switchTab } from '@tarojs/taro'; +import { useAd } from '../../../hooks/useAd'; + +interface SuccessComponentProps { + task: any; +} + +const SuccessComponent: React.FC = ({ task }) => { + const { + showAd, + loading: adLoading, + adAvailable, + } = useAd({ + onReward: () => { + handleDownloadImages(); + }, + onClose: isEnded => { + if (!isEnded) { + showToast({ + title: '请观看完整广告才能下载', + icon: 'none', + duration: 2000, + }); + } + }, + }); + const handleDownloadImages = async () => { + const mediaUrl = task?.outputUrl; + if (!mediaUrl) return; + + console.log({ mediaUrl }); + + try { + const getFileInfoFromUrl = (url: string) => { + const cleanUrl = url.split('?')[0].split('#')[0]; + const fileName = cleanUrl.substring(cleanUrl.lastIndexOf('/') + 1) || 'download'; + const extensionMatch = fileName.match(/\.([^.]+)$/); + const extension = extensionMatch ? extensionMatch[1].toLowerCase() : ''; + + return { fileName, extension, cleanUrl }; + }; + + const fileInfo = getFileInfoFromUrl(mediaUrl); + + let finalExtension = fileInfo.extension; + if (!finalExtension) { + finalExtension = task?.type === 'video' ? 'mp4' : 'jpg'; + } + + showToast({ title: '下载中...', icon: 'loading', duration: 1000 * 60 }); + const downloadRes = await downloadFile({ + url: mediaUrl, + header: { + Accept: 'image/*, video/*', + 'User-Agent': 'Mozilla/5.0 (compatible; MiniApp)', + }, + }); + hideToast(); + const filePath = downloadRes.tempFilePath; + + showToast({ title: '保存中...', icon: 'loading' }); + if (task?.type === 'video') { + console.log('保存为视频文件'); + await saveVideoToPhotosAlbum({ filePath }); + } else if (task?.type === 'image') { + console.log('保存为图片文件'); + await saveImageToPhotosAlbum({ filePath }); + } else { + console.log('文件类型不确定,尝试保存为图片'); + try { + await saveImageToPhotosAlbum({ filePath }); + } catch (imageError) { + console.log('图片保存失败,尝试视频保存'); + await saveVideoToPhotosAlbum({ filePath }); + } + } + + showToast({ + title: '保存成功', + icon: 'success', + duration: 2000, + }); + } catch (error) { + console.error('保存失败:', error); + + let errorMsg = '保存失败,请重试'; + if (error && typeof error === 'object') { + const err = error as any; + if (err.errMsg) { + console.error('详细错误信息:', err.errMsg); + if (err.errMsg.includes('auth')) { + errorMsg = '权限不足,请在设置中开启相册权限'; + } else if (err.errMsg.includes('network')) { + errorMsg = '网络错误,请检查网络连接'; + } else if (err.errMsg.includes('invalid file type')) { + errorMsg = '文件格式不支持,请联系客服'; + } else if (err.errMsg.includes('file')) { + errorMsg = '文件格式不支持或已损坏'; + } else if (err.errMsg.includes('download')) { + errorMsg = '文件下载失败,请重试'; + } + } + } + + showToast({ + title: errorMsg, + icon: 'error', + duration: 3000, + }); + } finally { + } + }; + + const getButtonText = () => { + if (adLoading) { + return '广告加载中...'; + } + if (!adAvailable) { + return '免费下载'; + } + return '观看广告下载'; + }; + + return ( + + {/* 毛玻璃背景 */} + + {/* 主要内容区域 */} + + + {task?.type === 'video' ? ( + + + + {/* 底部操作区域 */} + + + + {getButtonText()} + + + switchTab({ url: '/pages/home/index' })}> + 返回首页 + + + + ); +}; + +export default SuccessComponent; diff --git a/src/pages/result/index.css b/src/pages/result/index.css index dd0c218..14179d3 100644 --- a/src/pages/result/index.css +++ b/src/pages/result/index.css @@ -10,6 +10,12 @@ box-sizing: border-box; } +/* 生成中页面样式 */ +.result-page.generating { + background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%); + padding: 0; +} + /* 毛玻璃背景效果 */ .glassmorphism-background { position: absolute; @@ -105,3 +111,240 @@ font-weight: 400; color: #1d1f22; } + +/* 生成中内容区域 */ +.generating-content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + padding: 60px 32px; + box-sizing: border-box; +} + +/* 进度容器 */ +.progress-container { + position: relative; + margin-bottom: 60px; +} + +/* 进度方框 */ +.progress-circle { + width: 280px; + height: 280px; + border-radius: 20px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + background: white; + box-shadow: 0 4px 20px rgb(0 0 0 / 10%); +} + +.progress-fill { + position: absolute; + inset: -4px; + border-radius: 24px; + background: conic-gradient(#ff9500 0deg, #ff9500 0deg, transparent 0deg, transparent 360deg); + padding: 4px; + z-index: 1; +} + +.progress-inner { + width: 100%; + height: 100%; + border-radius: 20px; + background: white; + display: flex; + align-items: center; + justify-content: center; + position: relative; + z-index: 2; + overflow: hidden; +} + +.progress-text { + font-size: 48px; + font-weight: 600; + color: #333; +} + +/* 预览图片 */ +.preview-image { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + border-radius: 20px; + z-index: 3; +} + +/* 预览占位符 */ +.preview-placeholder { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + border-radius: 20px; + background: linear-gradient(135deg, #f0f0f0 0%, #e0e0e0 100%); + display: flex; + align-items: center; + justify-content: center; + z-index: 3; +} + +/* 光扫描动画 */ +.scan-light { + position: absolute; + inset: 0; + border-radius: 20px; + background: linear-gradient( + 90deg, + transparent 0%, + rgb(255 255 255 / 8%) 40%, + rgb(255 255 255 / 15%) 50%, + rgb(255 255 255 / 8%) 60%, + transparent 100% + ); + z-index: 4; + animation: scan 2s ease-in-out infinite; + pointer-events: none; +} + +@keyframes scan { + 0% { + transform: translateX(-100%); + } + + 100% { + transform: translateX(100%); + } +} + +.placeholder-icon { + font-size: 48px; + opacity: 0.6; +} + +/* 状态区域 */ +.status-section { + text-align: center; + margin-bottom: 80px; +} + +.status-icon { + font-size: 40px; + margin-bottom: 16px; +} + +.status-title { + font-size: 36px; + font-weight: 500; + color: #333; + margin-bottom: 12px; +} + +.status-subtitle { + font-size: 28px; + color: #666; +} + +/* 生成中操作按钮 */ +.generating-actions { + margin-bottom: 60px; +} + +.later-btn { + width: 320px; + height: 80px; + background: #333; + border-radius: 40px; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 32px; + font-weight: 500; +} + +/* 提示区域 */ +.tips-section { + text-align: center; +} + +.tips-label { + font-size: 24px; + color: #999; + letter-spacing: 2px; + margin-bottom: 12px; +} + +.tips-text { + font-size: 28px; + color: #666; +} + +/* 错误页面样式 */ +.result-page.error { + background: #f5f5f5; + padding: 0; +} + +.error-content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + padding: 60px 32px; + box-sizing: border-box; +} + +.error-container { + text-align: center; + margin-bottom: 80px; +} + +.error-text { + font-size: 36px; + font-weight: 500; + color: #333; + margin-bottom: 16px; +} + +.error-message { + font-size: 28px; + color: #666; +} + +.error-actions { + display: flex; + gap: 24px; +} + +.try-again-btn { + width: 160px; + height: 80px; + background: #333; + border-radius: 40px; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 28px; + font-weight: 500; +} + +.back-btn { + width: 120px; + height: 80px; + border: 1px solid #333; + border-radius: 40px; + display: flex; + align-items: center; + justify-content: center; + color: #333; + font-size: 28px; + font-weight: 500; +} diff --git a/src/pages/result/index.tsx b/src/pages/result/index.tsx index 5d7ea07..90b76b7 100644 --- a/src/pages/result/index.tsx +++ b/src/pages/result/index.tsx @@ -1,203 +1,96 @@ -import { Image, Video, View } from '@tarojs/components'; -import { - downloadFile, - getCurrentInstance, - hideToast, - navigateBack, - saveImageToPhotosAlbum, - saveVideoToPhotosAlbum, - showToast, - switchTab, -} from '@tarojs/taro'; -import { useEffect, useState } from 'react'; -import { useAd } from '../../hooks/useAd'; +import { getCurrentInstance, navigateBack, showToast } from '@tarojs/taro'; +import { useEffect, useRef, useState } from 'react'; +import { useServerSdk } from '../../hooks'; +import ErrorComponent from './components/ErrorComponent'; +import GeneratingComponent from './components/GeneratingComponent'; +import SuccessComponent from './components/SuccessComponent'; import './index.css'; const ResultPage: React.FC = () => { - const [images, setImages] = useState([]); - const [mediaType, setMediaType] = useState<'image' | 'video'>('image'); - const { - showAd, - loading: adLoading, - adAvailable, - } = useAd({ - onReward: () => { - handleDownloadImages(); - }, - onClose: isEnded => { - if (!isEnded) { - showToast({ - title: '请观看完整广告才能下载', - icon: 'none', - duration: 2000, - }); - } - }, - }); + const [taskId, setTaskId] = useState(null); + const [task, setTask] = useState(null); + const [error, setError] = useState(null); + const serverSdk = useServerSdk(); + const timerRef = useRef(null); + + // 轮询任务结果 + const pollTaskResult = async (taskId: string) => { + try { + let attempts = 0; + const maxAttempts = 30 * 20; // 最大轮询10分钟 + + const poll = async () => { + attempts++; + const result = await serverSdk.getTaskProgress(taskId); + setTask(result); + + // 检查任务状态,只有在进行中时才继续轮询 + if (result.status === 'completed') { + // 任务完成,停止轮询 + return; + } else if (result.status === 'failed') { + // 任务失败,停止轮询并设置错误 + setError(result.error || result.errorMessage || '任务执行失败'); + return; + } else if (result.status === 'processing' || result.status === 'pending') { + // 任务仍在进行中,继续轮询 + if (attempts < maxAttempts) { + timerRef.current = setTimeout(poll, 1000); + } else { + setError('处理超时'); + } + } else { + // 其他状态,停止轮询 + return; + } + }; + + poll(); + } catch (error) { + console.error('轮询任务失败:', error); + setError('获取任务信息失败'); + } + }; useEffect(() => { const instance = getCurrentInstance(); const params = instance?.router?.params; - if (params?.images) { - try { - const imageList = JSON.parse(decodeURIComponent(params.images)); - setImages(imageList); - - if (imageList.length > 0) { - const firstUrl = imageList[0]; - const isVideo = /\.(mp4|webm|ogg|mov|avi|mkv|flv)$/i.test(firstUrl); - setMediaType(isVideo ? 'video' : 'image'); - } - } catch (error) { - console.error('解析图片参数失败:', error); - showToast({ - title: '参数错误', - icon: 'error', - }); - navigateBack(); - } + if (params?.taskId) { + setTaskId(params.taskId); + // 开始轮询 + pollTaskResult(params.taskId); } else { showToast({ - title: '缺少图片参数', + title: '缺少taskId参数', icon: 'error', }); navigateBack(); } + + // 清理定时器 + return () => { + if (timerRef.current) { + clearTimeout(timerRef.current); + timerRef.current = null; + } + }; }, []); - const handleDownloadImages = async () => { - const mediaUrl = images.length > 0 ? images[0] : ''; - if (!mediaUrl) return; - - console.log({ mediaUrl }); - - try { - const getFileInfoFromUrl = (url: string) => { - const cleanUrl = url.split('?')[0].split('#')[0]; - const fileName = cleanUrl.substring(cleanUrl.lastIndexOf('/') + 1) || 'download'; - const extensionMatch = fileName.match(/\.([^.]+)$/); - const extension = extensionMatch ? extensionMatch[1].toLowerCase() : ''; - - return { fileName, extension, cleanUrl }; - }; - - const fileInfo = getFileInfoFromUrl(mediaUrl); - - let finalExtension = fileInfo.extension; - if (!finalExtension) { - finalExtension = mediaType === 'video' ? 'mp4' : 'jpg'; - } - - showToast({ title: '下载中...', icon: 'loading', duration: 1000 * 60 }); - const downloadRes = await downloadFile({ - url: mediaUrl, - header: { - Accept: 'image/*, video/*', - 'User-Agent': 'Mozilla/5.0 (compatible; MiniApp)', - }, - }); - hideToast(); - const filePath = downloadRes.tempFilePath; - - showToast({ title: '保存中...', icon: 'loading' }); - if (mediaType === 'video') { - console.log('保存为视频文件'); - await saveVideoToPhotosAlbum({ filePath }); - } else if (mediaType === 'image') { - console.log('保存为图片文件'); - await saveImageToPhotosAlbum({ filePath }); - } else { - console.log('文件类型不确定,尝试保存为图片'); - try { - await saveImageToPhotosAlbum({ filePath }); - } catch (imageError) { - console.log('图片保存失败,尝试视频保存'); - await saveVideoToPhotosAlbum({ filePath }); - } - } - - showToast({ - title: '保存成功', - icon: 'success', - duration: 2000, - }); - } catch (error) { - console.error('保存失败:', error); - - let errorMsg = '保存失败,请重试'; - if (error && typeof error === 'object') { - const err = error as any; - if (err.errMsg) { - console.error('详细错误信息:', err.errMsg); - if (err.errMsg.includes('auth')) { - errorMsg = '权限不足,请在设置中开启相册权限'; - } else if (err.errMsg.includes('network')) { - errorMsg = '网络错误,请检查网络连接'; - } else if (err.errMsg.includes('invalid file type')) { - errorMsg = '文件格式不支持,请联系客服'; - } else if (err.errMsg.includes('file')) { - errorMsg = '文件格式不支持或已损坏'; - } else if (err.errMsg.includes('download')) { - errorMsg = '文件下载失败,请重试'; - } - } - } - - showToast({ - title: errorMsg, - icon: 'error', - duration: 3000, - }); - } finally { - } - }; - - if (!images.length) { + if (!taskId) { return null; } - const getButtonText = () => { - if (adLoading) { - return '广告加载中...'; - } - if (!adAvailable) { - return '免费下载'; - } - return '观看广告下载'; - }; + if (error) { + return ; + } - return ( - - {/* 毛玻璃背景 */} - - {/* 主要内容区域 */} - - - {mediaType === 'video' ? ( - - + if (task?.status === 'completed' && task?.outputUrl) { + return ; + } - {/* 底部操作区域 */} - - - - {getButtonText()} - - {/* - {getViewButtonText()} - */} - - switchTab({ url: '/pages/home/index' })}> - 返回首页 - - - - ); + // 默认显示生成中状态 + return ; }; export default ResultPage; diff --git a/src/sdk/sdk-server.ts b/src/sdk/sdk-server.ts index a9c9def..aea3d7e 100644 --- a/src/sdk/sdk-server.ts +++ b/src/sdk/sdk-server.ts @@ -205,8 +205,8 @@ export class SdkServer { /** * 登录 - * @param params - * @returns + * @param params + * @returns */ async login(params: any): Promise<{ tokens: IToken, user: IUser }> { try { @@ -226,7 +226,7 @@ export class SdkServer { /** * 获取用户信息 可检测token是否过期 - * @returns + * @returns */ async profile() { try { @@ -259,13 +259,14 @@ export class SdkServer { /** * 获取任务执行详情 - * @param taskId - * @returns + * @param taskId + * @returns */ async getTaskProgress(taskId: string) { try { console.log(`task id is : ${taskId}`) const response = await this.request(`/api/v1/templates/execution/${taskId}/progress`, 'GET', {}); + console.log(`getTaskProgress response:`, response.data) return response.data } catch (error) { throw error; @@ -351,4 +352,4 @@ export class SdkServer { setTimeout(timeout: number): void { (this as any).timeout = timeout; } -} \ No newline at end of file +}