refactor(generate): 移除生成页面及相关资源,优化项目结构

- 删除生成页面的配置、样式和逻辑文件,简化项目结构
- 更新历史和首页逻辑,调整跳转至结果页面的参数
- 增强结果页面的任务轮询逻辑,提升用户体验
- 添加生成中和错误状态的组件,优化代码可读性
This commit is contained in:
iHeyTang
2025-09-11 14:43:35 +08:00
parent 60dfe3465a
commit ad5735b6a5
12 changed files with 614 additions and 617 deletions

View File

@@ -2,7 +2,6 @@ export default defineAppConfig({
pages: [
'pages/home/index', // 新的模板卡片首页
'pages/history/index', // 历史记录页面
'pages/generate/index', // 生成处理页面
'pages/result/index',
],
tabBar: {

View File

@@ -1,8 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '生成中',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
backgroundColorTop: '#ffffff',
backgroundColorBottom: '#ffffff',
backgroundColor: '#ffffff'
})

View File

@@ -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;
}

View File

@@ -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<any | null>(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 = () => (
<View className="generate-loading">
<LoadingOverlay>
<View className="action-buttons">
<Button className="btn-primary" onClick={handleTryAgain}>
</Button>
<Button className="btn-secondary" onClick={handleViewHistory}>
</Button>
</View>
</LoadingOverlay>
</View>
);
const renderError = () => (
<View className="generate-error">
<View className="error-container">
<Text className="error-text"></Text>
<Text className="error-message">{result?.error || '未知错误'}</Text>
</View>
<View className="action-buttons">
<Button className="btn-primary" onClick={handleTryAgain}>
</Button>
<Button className="btn-secondary" onClick={handleViewHistory}>
</Button>
</View>
</View>
);
return (
<View className="generate">
{status === 'loading' && renderLoading()}
{status === 'error' && renderError()}
</View>
);
}

View File

@@ -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}`,
});
}
};

View File

@@ -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) {
// 业务处理失败

View File

@@ -0,0 +1,33 @@
import { View } from '@tarojs/components';
import { navigateBack, switchTab } from '@tarojs/taro';
interface ErrorComponentProps {
error: string;
}
const ErrorComponent: React.FC<ErrorComponentProps> = ({ error }) => {
const handleTryAgain = () => {
switchTab({ url: '/pages/home/index' });
};
return (
<View className="result-page error">
<View className="error-content">
<View className="error-container">
<View className="error-text"></View>
<View className="error-message">{error}</View>
</View>
<View className="error-actions">
<View className="try-again-btn" onClick={handleTryAgain}>
</View>
<View className="back-btn" onClick={() => navigateBack()}>
</View>
</View>
</View>
</View>
);
};
export default ErrorComponent;

View File

@@ -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<GeneratingComponentProps> = ({ 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 (
<View className="result-page generating">
<View className="generating-content">
{/* 进度方框 */}
<View className="progress-container">
<View className="progress-circle">
<View
className="progress-fill"
style={{
background: `conic-gradient(#ff9500 ${progress * 3.6}deg, transparent 0deg)`,
}}
/>
<View className="progress-inner">
{/* 预览图片或占位符 */}
{task?.inputImageUrl ? (
<Image className="preview-image" src={task.inputImageUrl} mode="aspectFill" />
) : (
<View className="preview-placeholder">
<View className="placeholder-icon">🎨</View>
</View>
)}
{/* 光扫描动画 */}
<View className="scan-light" />
</View>
</View>
</View>
{/* 状态文本 */}
<View className="status-section">
<View className="status-title">... {Math.round(progress)}%</View>
<View className="status-subtitle"></View>
</View>
{/* 操作按钮 */}
<View className="generating-actions">
<View className="later-btn" onClick={handleLaterView}>
</View>
</View>
{/* 提示信息 */}
<View className="tips-section">
<View className="tips-label">· T I P S ·</View>
<View className="tips-text">退</View>
</View>
</View>
</View>
);
};
export default GeneratingComponent;

View File

@@ -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<SuccessComponentProps> = ({ 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 (
<View className="result-page">
{/* 毛玻璃背景 */}
<View className="glassmorphism-background" />
{/* 主要内容区域 */}
<View className="result-content">
<View className="result-container">
{task?.type === 'video' ? (
<Video className="result-video" src={task?.outputUrl} autoplay controls={false} muted loop />
) : (
<Image className="result-image" src={task?.outputUrl} mode="widthFix" />
)}
</View>
</View>
{/* 底部操作区域 */}
<View className="result-actions">
<View className="action-buttons">
<View className={`download-btn ${adLoading ? 'disabled' : ''} ${!adAvailable ? 'watched' : ''}`} onClick={adLoading ? undefined : showAd}>
{getButtonText()}
</View>
</View>
<View className="home-link" onClick={() => switchTab({ url: '/pages/home/index' })}>
</View>
</View>
</View>
);
};
export default SuccessComponent;

View File

@@ -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;
}

View File

@@ -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<string[]>([]);
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<string | null>(null);
const [task, setTask] = useState<any>(null);
const [error, setError] = useState<string | null>(null);
const serverSdk = useServerSdk();
const timerRef = useRef<NodeJS.Timeout | null>(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 <ErrorComponent error={error} />;
}
return (
<View className="result-page">
{/* 毛玻璃背景 */}
<View className="glassmorphism-background" />
{/* 主要内容区域 */}
<View className="result-content">
<View className="result-container">
{mediaType === 'video' ? (
<Video className="result-video" src={images[0]} autoplay controls={false} muted loop />
) : (
<Image className="result-image" src={images[0]} mode="widthFix" />
)}
</View>
</View>
if (task?.status === 'completed' && task?.outputUrl) {
return <SuccessComponent task={task} />;
}
{/* 底部操作区域 */}
<View className="result-actions">
<View className="action-buttons">
<View className={`download-btn ${adLoading ? 'disabled' : ''} ${!adAvailable ? 'watched' : ''}`} onClick={adLoading ? undefined : showAd}>
{getButtonText()}
</View>
{/* <View className="view-btn" onClick={handleViewMedia}>
{getViewButtonText()}
</View> */}
</View>
<View className="home-link" onClick={() => switchTab({ url: '/pages/home/index' })}>
</View>
</View>
</View>
);
// 默认显示生成中状态
return <GeneratingComponent task={task} />;
};
export default ResultPage;

View File

@@ -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<any>(`/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;
}
}
}