refactor(generate): 移除生成页面及相关资源,优化项目结构
- 删除生成页面的配置、样式和逻辑文件,简化项目结构 - 更新历史和首页逻辑,调整跳转至结果页面的参数 - 增强结果页面的任务轮询逻辑,提升用户体验 - 添加生成中和错误状态的组件,优化代码可读性
This commit is contained in:
@@ -2,7 +2,6 @@ export default defineAppConfig({
|
||||
pages: [
|
||||
'pages/home/index', // 新的模板卡片首页
|
||||
'pages/history/index', // 历史记录页面
|
||||
'pages/generate/index', // 生成处理页面
|
||||
'pages/result/index',
|
||||
],
|
||||
tabBar: {
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '生成中',
|
||||
navigationBarBackgroundColor: '#ffffff',
|
||||
navigationBarTextStyle: 'black',
|
||||
backgroundColorTop: '#ffffff',
|
||||
backgroundColorBottom: '#ffffff',
|
||||
backgroundColor: '#ffffff'
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
// 业务处理失败
|
||||
|
||||
33
src/pages/result/components/ErrorComponent.tsx
Normal file
33
src/pages/result/components/ErrorComponent.tsx
Normal 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;
|
||||
101
src/pages/result/components/GeneratingComponent.tsx
Normal file
101
src/pages/result/components/GeneratingComponent.tsx
Normal 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;
|
||||
155
src/pages/result/components/SuccessComponent.tsx
Normal file
155
src/pages/result/components/SuccessComponent.tsx
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user