style: 统一样式单位为px,优化代码格式和可读性
- 将CSS样式中的rpx单位替换为px,提升样式一致性 - 在TypeScript文件中添加缺失的分号,优化代码可读性 - 删除不再使用的组件及其样式,简化项目结构 - 更新生成页面和结果页面的样式,提升用户体验
This commit is contained in:
@@ -5,22 +5,19 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-bottom: 40rpx;
|
||||
padding-top: 20rpx;
|
||||
padding: 20px 0 40px;
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
width: 80%;
|
||||
height: 100rpx;
|
||||
height: 100px;
|
||||
background: linear-gradient(45deg, #ff6b6b, #ee5a24);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50rpx;
|
||||
font-size: 32rpx;
|
||||
border-radius: 50px;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20rpx;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -52,14 +49,14 @@
|
||||
|
||||
.regenerate-btn {
|
||||
width: 80%;
|
||||
height: 100rpx;
|
||||
height: 100px;
|
||||
background: linear-gradient(45deg, #52c41a, #73d13d);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50rpx;
|
||||
font-size: 32rpx;
|
||||
border-radius: 50px;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20rpx;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -72,6 +69,6 @@
|
||||
}
|
||||
|
||||
.download-tip {
|
||||
font-size: 24rpx;
|
||||
font-size: 24px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
@@ -1,67 +1,61 @@
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import './index.css'
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import './index.css';
|
||||
|
||||
interface DownloadSectionProps {
|
||||
onDownload: () => void
|
||||
onRegenerate?: () => void
|
||||
onImageToVideo?: () => void
|
||||
loading: boolean
|
||||
hasWatchedAd?: boolean
|
||||
adAvailable?: boolean
|
||||
showImageToVideo?: boolean
|
||||
imageToVideoLoading?: boolean
|
||||
onDownload: () => void;
|
||||
onRegenerate?: () => void;
|
||||
onImageToVideo?: () => void;
|
||||
loading: boolean;
|
||||
hasWatchedAd?: boolean;
|
||||
adAvailable?: boolean;
|
||||
showImageToVideo?: boolean;
|
||||
imageToVideoLoading?: boolean;
|
||||
}
|
||||
|
||||
const DownloadSection: React.FC<DownloadSectionProps> = ({
|
||||
onDownload,
|
||||
onRegenerate,
|
||||
onImageToVideo,
|
||||
loading,
|
||||
adAvailable = true,
|
||||
const DownloadSection: React.FC<DownloadSectionProps> = ({
|
||||
onDownload,
|
||||
onRegenerate,
|
||||
onImageToVideo,
|
||||
loading,
|
||||
adAvailable = true,
|
||||
showImageToVideo = false,
|
||||
imageToVideoLoading = false
|
||||
imageToVideoLoading = false,
|
||||
}) => {
|
||||
const getButtonText = () => {
|
||||
if (loading) {
|
||||
return '广告加载中...'
|
||||
return '广告加载中...';
|
||||
}
|
||||
if (!adAvailable) {
|
||||
return '📱 免费下载'
|
||||
return '📱 免费下载';
|
||||
}
|
||||
return '🎬 看广告下载'
|
||||
}
|
||||
return '🎬 看广告下载';
|
||||
};
|
||||
|
||||
const getTipText = () => {
|
||||
if (!adAvailable) {
|
||||
return '🎁 已为您跳过广告,可以直接免费下载'
|
||||
return '🎁 已为您跳过广告,可以直接免费下载';
|
||||
}
|
||||
return '观看完整广告即可免费下载所有图片'
|
||||
}
|
||||
return '观看完整广告即可免费下载所有图片';
|
||||
};
|
||||
|
||||
return (
|
||||
<View className='download-section'>
|
||||
<View
|
||||
className={`download-btn ${loading ? 'disabled' : ''} ${!adAvailable ? 'watched' : ''}`}
|
||||
onClick={loading ? undefined : onDownload}
|
||||
>
|
||||
<View className="download-section">
|
||||
<View className={`download-btn ${loading ? 'disabled' : ''} ${!adAvailable ? 'watched' : ''}`} onClick={loading ? undefined : onDownload}>
|
||||
<Text>{getButtonText()}</Text>
|
||||
</View>
|
||||
{onRegenerate && (
|
||||
<View className='regenerate-btn' onClick={onRegenerate}>
|
||||
<View className="regenerate-btn" onClick={onRegenerate}>
|
||||
<Text>🎨 再来一张</Text>
|
||||
</View>
|
||||
)}
|
||||
{showImageToVideo && onImageToVideo && (
|
||||
<View
|
||||
className={`regenerate-btn ${imageToVideoLoading ? 'disabled' : ''}`}
|
||||
onClick={imageToVideoLoading ? undefined : onImageToVideo}
|
||||
>
|
||||
<View className={`regenerate-btn ${imageToVideoLoading ? 'disabled' : ''}`} onClick={imageToVideoLoading ? undefined : onImageToVideo}>
|
||||
<Text>{imageToVideoLoading ? '🎬 生成中...' : '🎬 图片生视频'}</Text>
|
||||
</View>
|
||||
)}
|
||||
<Text className='download-tip'>{getTipText()}</Text>
|
||||
<Text className="download-tip">{getTipText()}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default DownloadSection
|
||||
export default DownloadSection;
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
.error-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.error-container {
|
||||
text-align: center;
|
||||
color: white;
|
||||
padding: 80rpx;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 96rpx;
|
||||
margin-bottom: 40rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.error-hint {
|
||||
font-size: 28rpx;
|
||||
opacity: 0.8;
|
||||
display: block;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import './index.css'
|
||||
|
||||
interface ErrorOverlayProps {
|
||||
onRetry: () => void
|
||||
}
|
||||
|
||||
const ErrorOverlay: React.FC<ErrorOverlayProps> = ({ onRetry }) => {
|
||||
return (
|
||||
<View className='error-overlay' onClick={onRetry}>
|
||||
<View className='error-container'>
|
||||
<Text className='error-icon'>⚠️</Text>
|
||||
<Text className='error-title'>生成失败</Text>
|
||||
<Text className='error-hint'>点击任意处重新开始</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default ErrorOverlay
|
||||
@@ -1,207 +0,0 @@
|
||||
.image-audit {
|
||||
width: 100%;
|
||||
padding: 20rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 上传区域 */
|
||||
.upload-area {
|
||||
width: 100%;
|
||||
height: 400rpx;
|
||||
border: 2rpx dashed #dcdcdc;
|
||||
border-radius: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #fafafa;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.upload-area:hover {
|
||||
border-color: #007aff;
|
||||
background-color: #f0f8ff;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
color: #666;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
/* 图片预览 */
|
||||
.image-preview {
|
||||
width: 100%;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
width: 100%;
|
||||
max-height: 400rpx;
|
||||
border-radius: 12rpx;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.action-buttons {
|
||||
margin: 20rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.audit-btn {
|
||||
width: 300rpx;
|
||||
height: 80rpx;
|
||||
background-color: #007aff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 40rpx;
|
||||
font-size: 32rpx;
|
||||
line-height: 80rpx;
|
||||
}
|
||||
|
||||
.audit-btn:hover {
|
||||
background-color: #0066cc;
|
||||
}
|
||||
|
||||
/* 审核中状态 */
|
||||
.auditing-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.auditing-text {
|
||||
font-size: 30rpx;
|
||||
color: #007aff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
width: 200rpx;
|
||||
height: 60rpx;
|
||||
background-color: #ff4444;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 30rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 60rpx;
|
||||
}
|
||||
|
||||
.cancel-btn:hover {
|
||||
background-color: #cc0000;
|
||||
}
|
||||
|
||||
/* 上传进度 */
|
||||
.upload-progress {
|
||||
width: 100%;
|
||||
height: 400rpx;
|
||||
border: 2rpx solid #007aff;
|
||||
border-radius: 12rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f0f8ff;
|
||||
gap: 15rpx;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
color: #007aff;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 重新上传按钮 */
|
||||
.reupload-btn {
|
||||
width: 300rpx;
|
||||
height: 80rpx;
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 40rpx;
|
||||
font-size: 32rpx;
|
||||
line-height: 80rpx;
|
||||
margin-top: 15rpx;
|
||||
}
|
||||
|
||||
.reupload-btn:hover {
|
||||
background-color: #5a6268;
|
||||
}
|
||||
|
||||
/* 审核结果 */
|
||||
.audit-result {
|
||||
margin-top: 20rpx;
|
||||
padding: 20rpx;
|
||||
border-radius: 12rpx;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.result-success {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.result-details {
|
||||
margin-top: 15rpx;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
font-size: 28rpx;
|
||||
margin-bottom: 8rpx;
|
||||
display: block;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.result-details-text {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-top: 10rpx;
|
||||
display: block;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.result-error {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
display: block;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 750px) {
|
||||
.image-audit {
|
||||
padding: 15rpx;
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
height: 300rpx;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
max-height: 300rpx;
|
||||
}
|
||||
|
||||
.audit-btn {
|
||||
width: 250rpx;
|
||||
height: 70rpx;
|
||||
font-size: 30rpx;
|
||||
line-height: 70rpx;
|
||||
}
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { View, Image, Text, Button } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useImageDetectionTaskManager, ImageAuditResult, AuditStatus, AuditConclusion } from '../../hooks/useImageDetectionTaskManager';
|
||||
import { imageUploader } from '../../utils/imageUploader';
|
||||
import './index.css';
|
||||
|
||||
interface ImageAuditProps {
|
||||
imageUrl?: string;
|
||||
auditResult?: ImageAuditResult;
|
||||
isAuditing?: boolean;
|
||||
currentTaskId?: string;
|
||||
onImageUpload?: (imageUrl: string) => void;
|
||||
onAuditComplete?: (result: ImageAuditResult) => void;
|
||||
onAuditError?: (error: Error) => void;
|
||||
}
|
||||
|
||||
const ImageAudit: React.FC<ImageAuditProps> = ({
|
||||
imageUrl: propImageUrl,
|
||||
auditResult,
|
||||
isAuditing: propIsAuditing,
|
||||
currentTaskId,
|
||||
onImageUpload,
|
||||
onAuditComplete,
|
||||
onAuditError
|
||||
}) => {
|
||||
const [imageUrl, setImageUrl] = useState<string>(propImageUrl || '');
|
||||
const [isAuditing, setIsAuditing] = useState<boolean>(propIsAuditing || false);
|
||||
const [taskId, setTaskId] = useState<string>(currentTaskId || '');
|
||||
const [result, setResult] = useState<ImageAuditResult | undefined>(auditResult);
|
||||
const [isUploading, setIsUploading] = useState<boolean>(false);
|
||||
const [uploadProgress, setUploadProgress] = useState<number>(0);
|
||||
|
||||
const {
|
||||
submitAuditTask,
|
||||
cancelAuditTask
|
||||
} = useImageDetectionTaskManager();
|
||||
|
||||
// 选择并上传图片
|
||||
const handleChooseImage = useCallback(async () => {
|
||||
try {
|
||||
const res = await Taro.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['original', 'compressed'],
|
||||
sourceType: ['album', 'camera']
|
||||
});
|
||||
|
||||
const tempFilePath = res.tempFilePaths[0];
|
||||
|
||||
// 显示本地图片预览
|
||||
setImageUrl(tempFilePath);
|
||||
|
||||
// 开始上传
|
||||
setIsUploading(true);
|
||||
setUploadProgress(0);
|
||||
|
||||
try {
|
||||
const uploadedUrl = await imageUploader.upload({
|
||||
filePath: tempFilePath,
|
||||
onProgress: (progress) => {
|
||||
setUploadProgress(progress);
|
||||
}
|
||||
});
|
||||
|
||||
// 上传成功,更新为服务器URL
|
||||
setImageUrl(uploadedUrl);
|
||||
onImageUpload?.(uploadedUrl);
|
||||
|
||||
Taro.showToast({
|
||||
title: '图片上传成功',
|
||||
icon: 'success'
|
||||
});
|
||||
} catch (uploadError) {
|
||||
console.error('图片上传失败:', uploadError);
|
||||
Taro.showToast({
|
||||
title: '图片上传失败',
|
||||
icon: 'error'
|
||||
});
|
||||
// 上传失败时清除图片
|
||||
setImageUrl('');
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
setUploadProgress(0);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('选择图片失败:', error);
|
||||
Taro.showToast({
|
||||
title: '选择图片失败',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
}, [onImageUpload]);
|
||||
|
||||
// 开始审核
|
||||
const handleStartAudit = useCallback(async () => {
|
||||
if (!imageUrl) {
|
||||
Taro.showToast({
|
||||
title: '请先选择图片',
|
||||
icon: 'error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否为临时文件路径,如果是则不能进行检测
|
||||
if (imageUrl.startsWith('http://tmp/') || imageUrl.includes('wxfile://')) {
|
||||
Taro.showToast({
|
||||
title: '请等待图片上传完成',
|
||||
icon: 'error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsAuditing(true);
|
||||
|
||||
try {
|
||||
const newTaskId = await submitAuditTask(
|
||||
{ imageUrl },
|
||||
// onProgress
|
||||
(status, progress) => {
|
||||
console.log('审核进度:', status, progress);
|
||||
},
|
||||
// onSuccess
|
||||
(auditSuccessResult) => {
|
||||
setIsAuditing(false);
|
||||
setResult(auditSuccessResult);
|
||||
onAuditComplete?.(auditSuccessResult);
|
||||
},
|
||||
// onError
|
||||
(error) => {
|
||||
setIsAuditing(false);
|
||||
onAuditError?.(error);
|
||||
}
|
||||
);
|
||||
|
||||
setTaskId(newTaskId);
|
||||
} catch (error) {
|
||||
setIsAuditing(false);
|
||||
onAuditError?.(error as Error);
|
||||
}
|
||||
}, [imageUrl, submitAuditTask, onAuditComplete, onAuditError]);
|
||||
|
||||
// 取消审核
|
||||
const handleCancelAudit = useCallback(async () => {
|
||||
if (taskId) {
|
||||
try {
|
||||
await cancelAuditTask(taskId);
|
||||
setIsAuditing(false);
|
||||
setTaskId('');
|
||||
} catch (error) {
|
||||
console.error('取消审核失败:', error);
|
||||
}
|
||||
}
|
||||
}, [taskId, cancelAuditTask]);
|
||||
|
||||
// 获取审核结果文本
|
||||
const getAuditResultText = (conclusion?: AuditConclusion): string => {
|
||||
console.log({ conclusion })
|
||||
switch (conclusion) {
|
||||
case AuditConclusion.PASS:
|
||||
return '通过';
|
||||
case AuditConclusion.REJECT:
|
||||
return '拒绝';
|
||||
case AuditConclusion.REVIEW:
|
||||
return '人工复审';
|
||||
case AuditConclusion.UNCERTAIN:
|
||||
return '不确定';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
};
|
||||
|
||||
// 获取风险等级文本
|
||||
const getRiskLevelText = (riskLevel?: 'low' | 'medium' | 'high'): string => {
|
||||
switch (riskLevel) {
|
||||
case 'low':
|
||||
return '低';
|
||||
case 'medium':
|
||||
return '中';
|
||||
case 'high':
|
||||
return '高';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View className='image-audit'>
|
||||
{/* 图片上传区域 */}
|
||||
{!imageUrl && !isUploading && (
|
||||
<View className='upload-area' onClick={handleChooseImage}>
|
||||
<Text className='upload-text'>点击上传图片进行审核</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 上传进度 */}
|
||||
{isUploading && (
|
||||
<View className='upload-progress'>
|
||||
<Text className='upload-text'>正在上传图片...</Text>
|
||||
<Text className='progress-text'>{uploadProgress}%</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 图片预览 */}
|
||||
{imageUrl && (
|
||||
<View className='image-preview'>
|
||||
<Image src={imageUrl} mode='aspectFit' className='preview-image' />
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
{imageUrl && !isUploading && (
|
||||
<View className='action-buttons'>
|
||||
{!isAuditing && !result && (
|
||||
<View>
|
||||
<Button className='audit-btn' onClick={handleStartAudit}>
|
||||
开始审核
|
||||
</Button>
|
||||
<Button className='reupload-btn' onClick={handleChooseImage}>
|
||||
重新选择图片
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{isAuditing && (
|
||||
<View className='auditing-section'>
|
||||
<Text className='auditing-text'>审核中...</Text>
|
||||
<Button className='cancel-btn' onClick={handleCancelAudit}>
|
||||
取消审核
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 审核结果 */}
|
||||
{result && (
|
||||
<View className='audit-result'>
|
||||
{result.status === AuditStatus.COMPLETED && result.conclusion && (
|
||||
<View className='result-success'>
|
||||
<Text className='result-title'>
|
||||
审核结果: {getAuditResultText(result.conclusion)}
|
||||
</Text>
|
||||
{result.result && (
|
||||
<View className='result-details'>
|
||||
<Text className='result-item'>
|
||||
安全评分: {Math.round((result.result.score || 0) * 100)}
|
||||
</Text>
|
||||
<Text className='result-item'>
|
||||
风险等级: {getRiskLevelText(result.result.riskLevel)}
|
||||
</Text>
|
||||
{result.result.details && (
|
||||
<Text className='result-details-text'>
|
||||
{result.result.details}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{result.status === AuditStatus.FAILED && (
|
||||
<View className='result-error'>
|
||||
<Text className='error-title'>审核失败</Text>
|
||||
{result.errorMessage && (
|
||||
<Text className='error-message'>{result.errorMessage}</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageAudit;
|
||||
@@ -4,7 +4,7 @@
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
background: rgb(0 0 0 / 80%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -19,53 +19,58 @@
|
||||
.loading-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border: 6rpx solid #f3f3f3;
|
||||
border-top: 6rpx solid #667eea;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border: 6px solid #f3f3f3;
|
||||
border-top: 6px solid #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 40rpx;
|
||||
margin: 0 auto 40px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-title {
|
||||
font-size: 36rpx;
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.loading-desc {
|
||||
font-size: 28rpx;
|
||||
font-size: 28px;
|
||||
opacity: 0.8;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.loading-container .action-buttons {
|
||||
margin-top: 60rpx;
|
||||
margin-top: 60px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
width: 240rpx;
|
||||
gap: 20px;
|
||||
width: 240px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loading-container .btn-primary,
|
||||
.loading-container .btn-secondary {
|
||||
border-radius: 25rpx;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
font-size: 28rpx;
|
||||
border-radius: 25px;
|
||||
height: 80px;
|
||||
line-height: 80px;
|
||||
font-size: 28px;
|
||||
border: none;
|
||||
width: 240rpx;
|
||||
width: 240px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -77,6 +82,6 @@
|
||||
}
|
||||
|
||||
.loading-container .btn-secondary {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
background: rgb(255 255 255 / 90%);
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import './index.css'
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import './index.css';
|
||||
|
||||
interface LoadingOverlayProps {
|
||||
children?: React.ReactNode
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ children }) => {
|
||||
return (
|
||||
<View className='loading-overlay'>
|
||||
<View className='loading-container'>
|
||||
<View className='loading-spinner' />
|
||||
<View className='loading-text'>
|
||||
<Text className='loading-title'>AI正在生成中...</Text>
|
||||
<Text className='loading-desc'>请耐心等待,正在为您精心制作</Text>
|
||||
<View className="loading-overlay">
|
||||
<View className="loading-container">
|
||||
<View className="loading-spinner" />
|
||||
<View className="loading-text">
|
||||
<Text className="loading-title">AI正在生成中...</Text>
|
||||
<Text className="loading-desc">请耐心等待,正在为您精心制作</Text>
|
||||
</View>
|
||||
{children}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingOverlay
|
||||
export default LoadingOverlay;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
.template-card {
|
||||
background: #fff;
|
||||
border-radius: 32rpx;
|
||||
border-radius: 32px;
|
||||
padding: 0;
|
||||
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 4px 24px rgb(0 0 0 / 8%);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -15,7 +15,6 @@
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/* 优化:简化active状态,减少重绘 */
|
||||
.template-card:active {
|
||||
opacity: 0.9;
|
||||
}
|
||||
@@ -25,17 +24,17 @@
|
||||
position: relative;
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
min-height: 480rpx;
|
||||
border-radius: 32rpx;
|
||||
min-height: 480px;
|
||||
border-radius: 32px;
|
||||
}
|
||||
|
||||
.merged-image-container {
|
||||
position: relative;
|
||||
border-radius: 32rpx;
|
||||
border-radius: 32px;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 480rpx;
|
||||
min-height: 480px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -45,13 +44,11 @@
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 480rpx;
|
||||
min-height: 480px;
|
||||
}
|
||||
|
||||
/* 优化:减少clip-path动画频率,提升性能 */
|
||||
.overlay-layer {
|
||||
transition: clip-path 0.2s ease-out;
|
||||
/* 启用硬件加速 */
|
||||
transform: translateZ(0);
|
||||
will-change: clip-path;
|
||||
}
|
||||
@@ -59,28 +56,21 @@
|
||||
.full-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 480rpx;
|
||||
min-height: 480px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
/* 性能优化:启用硬件加速和图片优化 */
|
||||
transform: translateZ(0);
|
||||
image-rendering: optimizeSpeed;
|
||||
/* 防止图片闪烁 */
|
||||
image-rendering: optimizespeed;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 可拖拽分割线 */
|
||||
.split-line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 8rpx; /* 增加宽度便于拖拽 */
|
||||
background: linear-gradient(to bottom,
|
||||
rgba(255, 255, 255, 0.8) 0%,
|
||||
rgba(255, 255, 255, 0.9) 50%,
|
||||
rgba(255, 255, 255, 0.8) 100%);
|
||||
width: 8px; /* 增加宽度便于拖拽 */
|
||||
background: linear-gradient(to bottom, rgb(255 255 255 / 80%) 0%, rgb(255 255 255 / 90%) 50%, rgb(255 255 255 / 80%) 100%);
|
||||
transform: translateX(-50%);
|
||||
z-index: 3;
|
||||
cursor: col-resize;
|
||||
@@ -93,15 +83,15 @@
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background: rgb(255 255 255 / 95%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.25);
|
||||
border: 4rpx solid rgba(0, 122, 255, 0.4);
|
||||
box-shadow: 0 4px 24px rgb(0 0 0 / 25%);
|
||||
border: 4px solid rgb(0 122 255 / 40%);
|
||||
cursor: col-resize;
|
||||
touch-action: none;
|
||||
transition: all 0.2s ease;
|
||||
@@ -109,12 +99,12 @@
|
||||
|
||||
.split-handle:active {
|
||||
transform: translate(-50%, -50%) scale(1.1);
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0 8px 32px rgb(0 0 0 / 30%);
|
||||
}
|
||||
|
||||
.split-icon {
|
||||
font-size: 24rpx;
|
||||
color: #007AFF;
|
||||
font-size: 24px;
|
||||
color: #007aff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -123,9 +113,9 @@
|
||||
/* 模板名称悬浮 - 图片底部 */
|
||||
.name-overlay {
|
||||
position: absolute;
|
||||
bottom: 12rpx;
|
||||
left: 24rpx;
|
||||
right: 24rpx;
|
||||
bottom: 12px;
|
||||
left: 24px;
|
||||
right: 24px;
|
||||
z-index: 4;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -134,15 +124,14 @@
|
||||
}
|
||||
|
||||
.name-badge {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(20rpx);
|
||||
-webkit-backdrop-filter: blur(20rpx);
|
||||
background: rgb(0 0 0 / 70%);
|
||||
backdrop-filter: blur(20px);
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 20rpx;
|
||||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3);
|
||||
padding: 8px 20px;
|
||||
border-radius: 20px;
|
||||
text-shadow: 0 2px 4px rgb(0 0 0 / 30%);
|
||||
text-align: center;
|
||||
max-width: 90%;
|
||||
width: fit-content;
|
||||
@@ -154,25 +143,23 @@
|
||||
/* 单视频容器样式 */
|
||||
.single-video-container {
|
||||
position: relative;
|
||||
border-radius: 32rpx;
|
||||
border-radius: 32px;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
min-height: 480rpx;
|
||||
min-height: 480px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.single-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 480rpx;
|
||||
min-height: 480px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
border-radius: 32rpx;
|
||||
border-radius: 32px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
/* 性能优化:启用硬件加速 */
|
||||
transform: translateZ(0);
|
||||
/* 防止视频闪烁 */
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
@@ -185,27 +172,23 @@
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
object-fit: cover;
|
||||
border-radius: 32rpx;
|
||||
border-radius: 32px;
|
||||
}
|
||||
|
||||
|
||||
/* 视频样式 */
|
||||
.full-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 480rpx;
|
||||
min-height: 480px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
/* 性能优化:启用硬件加速 */
|
||||
transform: translateZ(0);
|
||||
/* 防止视频闪烁 */
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.watermark-text {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 20rpx;
|
||||
color: rgb(255 255 255 / 50%);
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,186 +1,165 @@
|
||||
import { View, Text, Image, Video } from '@tarojs/components'
|
||||
import { useState, useRef, useMemo } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { Template } from '../../store/types'
|
||||
import './index.css'
|
||||
import { View, Text, Image, Video } from '@tarojs/components';
|
||||
import { useState, useRef, useMemo } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { Template } from '../../store/types';
|
||||
import './index.css';
|
||||
|
||||
interface TemplateCardProps {
|
||||
template: Template
|
||||
onClick: (template: Template) => void
|
||||
template: Template;
|
||||
onClick: (template: Template) => void;
|
||||
}
|
||||
|
||||
export default function TemplateCard({ template, onClick }: TemplateCardProps) {
|
||||
const [splitPosition, setSplitPosition] = useState(50) // 分割线位置百分比
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [containerInfo, setContainerInfo] = useState<any>(null)
|
||||
const containerRef = useRef<any>(null)
|
||||
const containerId = `container-${template.code}` // 为每个容器创建唯一ID
|
||||
const [splitPosition, setSplitPosition] = useState(50); // 分割线位置百分比
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [containerInfo, setContainerInfo] = useState<any>(null);
|
||||
const containerRef = useRef<any>(null);
|
||||
const containerId = `container-${template.code}`; // 为每个容器创建唯一ID
|
||||
|
||||
// 检测output是否为视频
|
||||
const isOutputVideo = useMemo(() => {
|
||||
return /\.(mp4|webm|ogg|mov|avi|mkv|flv)$/i.test(template.outputExample)
|
||||
}, [template.outputExample])
|
||||
return /\.(mp4|webm|ogg|mov|avi|mkv|flv)$/i.test(template.outputExample);
|
||||
}, [template.outputExample]);
|
||||
|
||||
// 检测input是否为视频
|
||||
const isInputVideo = useMemo(() => {
|
||||
return /\.(mp4|webm|ogg|mov|avi|mkv|flv)$/i.test(template.inputExample)
|
||||
}, [template.inputExample])
|
||||
return /\.(mp4|webm|ogg|mov|avi|mkv|flv)$/i.test(template.inputExample);
|
||||
}, [template.inputExample]);
|
||||
|
||||
const handleClick = () => {
|
||||
if (!isDragging) {
|
||||
onClick(template)
|
||||
onClick(template);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 获取容器信息
|
||||
const getContainerInfo = () => {
|
||||
return new Promise((resolve) => {
|
||||
const query = Taro.createSelectorQuery()
|
||||
query.select(`#${containerId}`).boundingClientRect((rect) => {
|
||||
if (rect) {
|
||||
setContainerInfo(rect)
|
||||
resolve(rect)
|
||||
}
|
||||
}).exec()
|
||||
})
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
const query = Taro.createSelectorQuery();
|
||||
query
|
||||
.select(`#${containerId}`)
|
||||
.boundingClientRect(rect => {
|
||||
if (rect) {
|
||||
setContainerInfo(rect);
|
||||
resolve(rect);
|
||||
}
|
||||
})
|
||||
.exec();
|
||||
});
|
||||
};
|
||||
|
||||
// 处理触摸开始
|
||||
const handleTouchStart = async (e: any) => {
|
||||
e.stopPropagation()
|
||||
setIsDragging(true)
|
||||
e.stopPropagation();
|
||||
setIsDragging(true);
|
||||
// 获取容器信息用于后续计算
|
||||
await getContainerInfo()
|
||||
}
|
||||
await getContainerInfo();
|
||||
};
|
||||
|
||||
// 处理触摸移动 - 优化:添加节流,减少计算频率
|
||||
const handleTouchMove = (e: any) => {
|
||||
if (!isDragging || !containerInfo) return
|
||||
e.stopPropagation()
|
||||
if (!isDragging || !containerInfo) return;
|
||||
e.stopPropagation();
|
||||
|
||||
const touch = e.touches[0]
|
||||
if (!touch) return
|
||||
const touch = e.touches[0];
|
||||
if (!touch) return;
|
||||
|
||||
// 使用requestAnimationFrame节流,提升性能
|
||||
requestAnimationFrame(() => {
|
||||
// 计算触摸点相对于容器的位置
|
||||
const touchX = touch.clientX - containerInfo.left
|
||||
const percentage = Math.max(10, Math.min(90, (touchX / containerInfo.width) * 100))
|
||||
const touchX = touch.clientX - containerInfo.left;
|
||||
const percentage = Math.max(10, Math.min(90, (touchX / containerInfo.width) * 100));
|
||||
|
||||
setSplitPosition(percentage)
|
||||
})
|
||||
}
|
||||
setSplitPosition(percentage);
|
||||
});
|
||||
};
|
||||
|
||||
// 处理触摸结束
|
||||
const handleTouchEnd = (e: any) => {
|
||||
e.stopPropagation()
|
||||
setTimeout(() => setIsDragging(false), 100) // 延迟重置,避免触发点击
|
||||
}
|
||||
e.stopPropagation();
|
||||
setTimeout(() => setIsDragging(false), 100); // 延迟重置,避免触发点击
|
||||
};
|
||||
|
||||
return (
|
||||
<View className='template-card' onClick={handleClick}>
|
||||
<View className="template-card" onClick={handleClick}>
|
||||
{/* 根据output类型显示不同的内容 */}
|
||||
{isOutputVideo ? (
|
||||
// 当output是视频时,只显示单个视频
|
||||
<View className='single-video-container'>
|
||||
<Image
|
||||
className='video-poster'
|
||||
src={template.inputExample}
|
||||
mode='aspectFill'
|
||||
/>
|
||||
<View className="single-video-container">
|
||||
<Image className="video-poster" src={template.inputExample} mode="aspectFill" />
|
||||
<Video
|
||||
className='single-video'
|
||||
className="single-video"
|
||||
src={template.outputExample}
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
objectFit='cover'
|
||||
objectFit="cover"
|
||||
showPlayBtn={false}
|
||||
showCenterPlayBtn={false}
|
||||
showFullscreenBtn={false}
|
||||
controls={false}
|
||||
/>
|
||||
{/* 模板名称悬浮 - 视频底部 */}
|
||||
<View className='name-overlay'>
|
||||
<Text className='name-badge'>
|
||||
{template.name}
|
||||
</Text>
|
||||
<Text className='watermark-text'>内容由AI生成</Text>
|
||||
<View className="name-overlay">
|
||||
<Text className="name-badge">{template.name}</Text>
|
||||
<Text className="watermark-text">内容由AI生成</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
// 原有的图片对比逻辑
|
||||
<View className='image-comparison'>
|
||||
<View
|
||||
id={containerId}
|
||||
className='merged-image-container'
|
||||
ref={containerRef}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
>
|
||||
<View className="image-comparison">
|
||||
<View id={containerId} className="merged-image-container" ref={containerRef} onTouchMove={handleTouchMove} onTouchEnd={handleTouchEnd}>
|
||||
{/* 原图层 - 完整图片/视频 */}
|
||||
<View className='image-layer'>
|
||||
<View className="image-layer">
|
||||
{isInputVideo ? (
|
||||
<Video
|
||||
className='full-video'
|
||||
className="full-video"
|
||||
src={template.inputExample}
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
objectFit='cover'
|
||||
objectFit="cover"
|
||||
showPlayBtn={false}
|
||||
showCenterPlayBtn={false}
|
||||
showFullscreenBtn={false}
|
||||
controls={false}
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
className='full-image'
|
||||
src={template.inputExample}
|
||||
mode='aspectFill'
|
||||
lazyLoad
|
||||
/>
|
||||
<Image className="full-image" src={template.inputExample} mode="aspectFill" lazyLoad />
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 效果图层 - 完整图片,通过遮罩显示右半部分 */}
|
||||
<View
|
||||
className='image-layer overlay-layer'
|
||||
className="image-layer overlay-layer"
|
||||
style={{
|
||||
clipPath: `polygon(${splitPosition}% 0%, 100% 0%, 100% 100%, ${splitPosition}% 100%)`
|
||||
clipPath: `polygon(${splitPosition}% 0%, 100% 0%, 100% 100%, ${splitPosition}% 100%)`,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
className='full-image'
|
||||
src={template.outputExample}
|
||||
mode='aspectFill'
|
||||
lazyLoad
|
||||
/>
|
||||
<Image className="full-image" src={template.outputExample} mode="aspectFill" lazyLoad />
|
||||
</View>
|
||||
|
||||
{/* 可拖拽的分割线 */}
|
||||
<View
|
||||
className='split-line'
|
||||
className="split-line"
|
||||
style={{ left: `${splitPosition}%` }}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
>
|
||||
<View className='split-handle'>
|
||||
<Text className='split-icon'>⟷</Text>
|
||||
<View className="split-handle">
|
||||
<Text className="split-icon">⟷</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 模板名称悬浮 - 图片底部 */}
|
||||
<View className='name-overlay'>
|
||||
<Text className='name-badge'>
|
||||
{template.name}
|
||||
</Text>
|
||||
<Text className='watermark-text'>内容由AI生成</Text>
|
||||
<View className="name-overlay">
|
||||
<Text className="name-badge">{template.name}</Text>
|
||||
<Text className="watermark-text">内容由AI生成</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
.button-container {
|
||||
position: absolute;
|
||||
bottom: 20vh;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
|
||||
.create-button {
|
||||
background: linear-gradient(135deg, #ff6b9d, #ff9a56, #ffd93d);
|
||||
border: none;
|
||||
border-radius: 144rpx;
|
||||
padding: 24rpx 216rpx;
|
||||
font-size: 72rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
text-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0 16rpx 50rpx rgba(255, 107, 157, 0.4),
|
||||
0 8rpx 30rpx rgba(255, 154, 86, 0.3), inset 0 4rpx 0 rgba(255, 255, 255, 0.3);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
cursor: pointer;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.create-button::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.4),
|
||||
transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.create-button:hover {
|
||||
transform: scale(1.05) translateY(-2px);
|
||||
box-shadow: 0 12px 35px rgba(255, 107, 157, 0.5),
|
||||
0 6px 20px rgba(255, 154, 86, 0.4), inset 0 2px 0 rgba(255, 255, 255, 0.4);
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.create-button:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.create-button:active {
|
||||
transform: scale(0.98) translateY(1px);
|
||||
box-shadow: 0 4px 15px rgba(255, 107, 157, 0.3),
|
||||
0 2px 8px rgba(255, 154, 86, 0.2), inset 0 2px 0 rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.create-button::after {
|
||||
content: "✨";
|
||||
position: absolute;
|
||||
top: -10rpx;
|
||||
right: -10rpx;
|
||||
font-size: 32rpx;
|
||||
animation: sparkle 2s infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { View, Button } from '@tarojs/components'
|
||||
import './index.css'
|
||||
|
||||
interface UploadButtonProps {
|
||||
onUpload: () => void
|
||||
}
|
||||
|
||||
const UploadButton: React.FC<UploadButtonProps> = ({ onUpload }) => {
|
||||
return (
|
||||
<View className='button-container'>
|
||||
<Button className='create-button' onClick={onUpload}>
|
||||
一键制作
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default UploadButton
|
||||
@@ -10,39 +10,39 @@
|
||||
|
||||
.generate-loading {
|
||||
text-align: center;
|
||||
padding: 60rpx;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
margin-bottom: 40rpx;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 500rpx;
|
||||
height: 12rpx;
|
||||
width: 250px;
|
||||
height: 6px;
|
||||
background-color: #e9ecef;
|
||||
border-radius: 6rpx;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 20rpx;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #007aff 0%, #34c759 100%);
|
||||
border-radius: 6rpx;
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 28rpx;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 32rpx;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
margin-top: 20rpx;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.generate-success {
|
||||
@@ -52,42 +52,41 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
padding: 40rpx 32rpx;
|
||||
padding: 20px 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 成功页面头部 */
|
||||
.success-header {
|
||||
text-align: center;
|
||||
margin-bottom: 40rpx;
|
||||
padding-top: 80rpx;
|
||||
margin-bottom: 20px;
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
.success-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 24rpx;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
font-size: 64rpx;
|
||||
filter: drop-shadow(0 4rpx 8rpx rgba(0, 0, 0, 0.1));
|
||||
font-size: 32px;
|
||||
filter: drop-shadow(0 2px 4px rgb(0 0 0 / 10%));
|
||||
}
|
||||
|
||||
.success-title {
|
||||
font-size: 48rpx;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
letter-spacing: -0.5rpx;
|
||||
letter-spacing: -0.25px;
|
||||
}
|
||||
|
||||
.success-subtitle {
|
||||
font-size: 28rpx;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
@@ -99,21 +98,21 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 40rpx;
|
||||
margin-bottom: 20px;
|
||||
min-height: 0;
|
||||
padding: 0 16rpx;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
border-radius: 32rpx;
|
||||
border-radius: 16px;
|
||||
padding: 0;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
|
||||
border: 1rpx solid #f0f0f0;
|
||||
box-shadow: 0 4px 16px rgb(0 0 0 / 10%);
|
||||
border: 0.5px solid #f0f0f0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 90%;
|
||||
max-width: 600rpx;
|
||||
min-height: 680rpx;
|
||||
max-width: 300px;
|
||||
min-height: 340px;
|
||||
height: 60vh;
|
||||
display: block;
|
||||
}
|
||||
@@ -121,22 +120,22 @@
|
||||
.preview-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 680rpx;
|
||||
min-height: 340px;
|
||||
object-fit: cover;
|
||||
border-radius: 32rpx;
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
transform: translateZ(0);
|
||||
image-rendering: optimizeSpeed;
|
||||
image-rendering: optimizespeed;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.preview-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 680rpx;
|
||||
min-height: 340px;
|
||||
object-fit: cover;
|
||||
border-radius: 32rpx;
|
||||
border-radius: 16px;
|
||||
display: block;
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
@@ -170,7 +169,7 @@
|
||||
|
||||
.generate-error {
|
||||
text-align: center;
|
||||
padding: 60rpx;
|
||||
padding: 30px;
|
||||
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
@@ -180,33 +179,33 @@
|
||||
}
|
||||
|
||||
.error-container {
|
||||
margin-bottom: 60rpx;
|
||||
margin-bottom: 30px;
|
||||
background: #fff;
|
||||
border-radius: 32rpx;
|
||||
padding: 60rpx 40rpx;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
|
||||
border: 1rpx solid #f0f0f0;
|
||||
max-width: 600rpx;
|
||||
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: 40rpx;
|
||||
font-size: 20px;
|
||||
color: #ff3b30;
|
||||
font-weight: 700;
|
||||
margin-bottom: 20rpx;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.error-text::before {
|
||||
content: '⚠️';
|
||||
font-size: 36rpx;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 30rpx;
|
||||
font-size: 15px;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
@@ -214,44 +213,43 @@
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 24rpx;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
max-width: 600rpx;
|
||||
margin: 0 auto 32rpx;
|
||||
max-width: 300px;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
|
||||
.success-actions .action-buttons {
|
||||
margin-bottom: 24rpx;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* 结果提示 */
|
||||
.result-tips {
|
||||
text-align: center;
|
||||
margin-top: 16rpx;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.tips-text {
|
||||
font-size: 24rpx;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
padding: 12rpx 24rpx;
|
||||
border-radius: 20rpx;
|
||||
background: rgb(255 255 255 / 80%);
|
||||
padding: 6px 12px;
|
||||
border-radius: 10px;
|
||||
display: inline-block;
|
||||
backdrop-filter: blur(10rpx);
|
||||
-webkit-backdrop-filter: blur(10rpx);
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
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%);
|
||||
background: linear-gradient(135deg, #ffd67a 0%, #ffc947 100%);
|
||||
color: #333;
|
||||
border-radius: 24rpx;
|
||||
height: 96rpx;
|
||||
font-size: 32rpx;
|
||||
border-radius: 12px;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
box-shadow: 0 8rpx 24rpx rgba(255, 214, 122, 0.3);
|
||||
box-shadow: 0 4px 12px rgb(255 214 122 / 30%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
@@ -266,7 +264,7 @@
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
||||
background: linear-gradient(90deg, transparent, rgb(255 255 255 / 30%), transparent);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -276,16 +274,15 @@
|
||||
|
||||
.btn-secondary {
|
||||
flex: 1;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
background: rgb(255 255 255 / 90%);
|
||||
color: #333;
|
||||
border: 2rpx solid rgba(255, 214, 122, 0.6);
|
||||
border-radius: 24rpx;
|
||||
height: 96rpx;
|
||||
font-size: 32rpx;
|
||||
border: 1px solid rgb(255 214 122 / 60%);
|
||||
border-radius: 12px;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
|
||||
backdrop-filter: blur(10rpx);
|
||||
-webkit-backdrop-filter: blur(10rpx);
|
||||
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
|
||||
backdrop-filter: blur(5px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -298,4 +295,4 @@
|
||||
.btn-text {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,127 +1,121 @@
|
||||
import { View, Text, Button } from '@tarojs/components'
|
||||
import { useEffect, useState } from 'react'
|
||||
import Taro, { switchTab, navigateTo } from '@tarojs/taro'
|
||||
import { useServerSdk } from '../../hooks/index'
|
||||
import LoadingOverlay from '../../components/LoadingOverlay'
|
||||
import './index.css'
|
||||
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
|
||||
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
|
||||
const instance = Taro.getCurrentInstance();
|
||||
const params = instance.router?.params;
|
||||
if (params?.taskId) {
|
||||
pollTaskResult(params.taskId)
|
||||
pollTaskResult(params.taskId);
|
||||
} else {
|
||||
setStatus('error')
|
||||
setResult({ success: false, error: '缺少必要参数' })
|
||||
setStatus('error');
|
||||
setResult({ success: false, error: '缺少必要参数' });
|
||||
}
|
||||
|
||||
// 页面离开时清理定时器
|
||||
return () => {
|
||||
if (timerRef) {
|
||||
clearTimeout(timerRef)
|
||||
timerRef = null
|
||||
clearTimeout(timerRef);
|
||||
timerRef = null;
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
};
|
||||
}, []);
|
||||
|
||||
const pollTaskResult = async (taskId: string) => {
|
||||
try {
|
||||
let attempts = 0
|
||||
const maxAttempts = 30 * 20 // 最多轮询30次(30秒)
|
||||
let attempts = 0;
|
||||
const maxAttempts = 30 * 20;
|
||||
|
||||
const poll = async () => {
|
||||
attempts++
|
||||
attempts++;
|
||||
|
||||
try {
|
||||
const result = await serverSdk.getTaskProgress(taskId)
|
||||
const result = await serverSdk.getTaskProgress(taskId);
|
||||
|
||||
if (result.status === 'completed') {
|
||||
setStatus('success')
|
||||
setStatus('success');
|
||||
navigateTo({
|
||||
url: `/pages/result/index?images=${encodeURIComponent(JSON.stringify([result.outputUrl]))}`
|
||||
})
|
||||
url: `/pages/result/index?images=${encodeURIComponent(JSON.stringify([result.outputUrl]))}`,
|
||||
});
|
||||
return;
|
||||
} else if (result.status === 'failed') {
|
||||
setStatus('error')
|
||||
setResult({ success: false, error: result.error || '处理失败' })
|
||||
setStatus('error');
|
||||
setResult({ success: false, error: result.error || '处理失败' });
|
||||
} else if (attempts < maxAttempts) {
|
||||
// 继续轮询
|
||||
timerRef = setTimeout(poll, 1000)
|
||||
timerRef = setTimeout(poll, 1000);
|
||||
} else {
|
||||
// 超时
|
||||
setStatus('error')
|
||||
setResult({ success: false, error: '处理超时' })
|
||||
setStatus('error');
|
||||
setResult({ success: false, error: '处理超时' });
|
||||
}
|
||||
} catch (error) {
|
||||
if (attempts < maxAttempts) {
|
||||
timerRef = setTimeout(poll, 1000)
|
||||
timerRef = setTimeout(poll, 1000);
|
||||
} else {
|
||||
setStatus('error')
|
||||
setResult({ success: false, error: '获取结果失败' })
|
||||
setStatus('error');
|
||||
setResult({ success: false, error: '获取结果失败' });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
poll()
|
||||
poll();
|
||||
} catch (error) {
|
||||
setStatus('error')
|
||||
setResult({ success: false, error: '开始处理失败' })
|
||||
setStatus('error');
|
||||
setResult({ success: false, error: '开始处理失败' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleTryAgain = () => {
|
||||
// 再来一张,返回首页
|
||||
switchTab({ url: '/pages/home/index' })
|
||||
}
|
||||
switchTab({ url: '/pages/home/index' });
|
||||
};
|
||||
|
||||
const handleViewHistory = () => {
|
||||
// 查看记录,切换到历史记录页
|
||||
switchTab({ url: '/pages/history/index' })
|
||||
}
|
||||
switchTab({ url: '/pages/history/index' });
|
||||
};
|
||||
|
||||
const renderLoading = () => (
|
||||
<View className='generate-loading'>
|
||||
<View className="generate-loading">
|
||||
<LoadingOverlay>
|
||||
<View className='action-buttons'>
|
||||
<Button className='btn-primary' onClick={handleTryAgain}>
|
||||
<View className="action-buttons">
|
||||
<Button className="btn-primary" onClick={handleTryAgain}>
|
||||
再来一张
|
||||
</Button>
|
||||
<Button className='btn-secondary' onClick={handleViewHistory}>
|
||||
<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 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}>
|
||||
<View className="action-buttons">
|
||||
<Button className="btn-primary" onClick={handleTryAgain}>
|
||||
再来一张
|
||||
</Button>
|
||||
<Button className='btn-secondary' onClick={handleViewHistory}>
|
||||
<Button className="btn-secondary" onClick={handleViewHistory}>
|
||||
查看记录
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<View className='generate'>
|
||||
<View className="generate">
|
||||
{status === 'loading' && renderLoading()}
|
||||
{status === 'error' && renderError()}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
.result-page {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -10,31 +9,29 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 40rpx;
|
||||
background: transparent;
|
||||
padding: 20px;
|
||||
color: white;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
border-bottom: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
border-bottom: 1px solid rgb(255 255 255 / 10%);
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-size: 36rpx;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 40rpx;
|
||||
font-size: 48rpx;
|
||||
line-height: 80rpx;
|
||||
border-radius: 20px;
|
||||
font-size: 24px;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -50,8 +47,6 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -62,16 +57,7 @@
|
||||
.backdrop-blur::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 255, 255, 0.1) 0%,
|
||||
rgba(255, 255, 255, 0.05) 50%,
|
||||
rgba(0, 0, 0, 0.1) 100%
|
||||
);
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -83,20 +69,20 @@
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
border-radius: 0rpx;
|
||||
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.3);
|
||||
border-radius: 0;
|
||||
box-shadow: 0 10px 30px rgb(0 0 0 / 30%);
|
||||
}
|
||||
|
||||
/* 模板处理结果对比样式 */
|
||||
.template-name {
|
||||
font-size: 36rpx;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8rpx;
|
||||
margin-bottom: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.result-status {
|
||||
font-size: 28rpx;
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -107,8 +93,8 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40rpx 32rpx;
|
||||
gap: 32rpx;
|
||||
padding: 20px 16px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.comparison-item {
|
||||
@@ -121,23 +107,22 @@
|
||||
|
||||
.comparison-image {
|
||||
width: 100%;
|
||||
max-height: 600rpx;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.2);
|
||||
background: #fff;
|
||||
max-height: 300px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgb(0 0 0 / 20%);
|
||||
}
|
||||
|
||||
.comparison-label {
|
||||
margin-top: 16rpx;
|
||||
margin-top: 8px;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.comparison-arrow {
|
||||
color: #fff;
|
||||
font-size: 48rpx;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
opacity: 0.8;
|
||||
}
|
||||
@@ -151,12 +136,11 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: black;
|
||||
}
|
||||
|
||||
.result-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 80vh;
|
||||
border-radius: 0rpx;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
@@ -1,239 +1,149 @@
|
||||
import { View, Image, Video } from '@tarojs/components'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { saveImageToPhotosAlbum, saveVideoToPhotosAlbum, navigateTo, downloadFile, showToast, navigateBack, getCurrentInstance, hideToast } from '@tarojs/taro'
|
||||
import DownloadSection from '../../components/DownloadSection'
|
||||
import { useAd } from '../../hooks/useAd'
|
||||
import { useServerSdk } from '../../hooks/index'
|
||||
import './index.css'
|
||||
import { Image, Video, View } from '@tarojs/components';
|
||||
import {
|
||||
downloadFile,
|
||||
getCurrentInstance,
|
||||
hideToast,
|
||||
navigateBack,
|
||||
navigateTo,
|
||||
saveImageToPhotosAlbum,
|
||||
saveVideoToPhotosAlbum,
|
||||
showToast,
|
||||
} from '@tarojs/taro';
|
||||
import { useEffect, useState } from 'react';
|
||||
import DownloadSection from '../../components/DownloadSection';
|
||||
import { useServerSdk } from '../../hooks/index';
|
||||
import { useAd } from '../../hooks/useAd';
|
||||
import './index.css';
|
||||
|
||||
const ResultPage: React.FC = () => {
|
||||
const [images, setImages] = useState<string[]>([])
|
||||
const [mediaType, setMediaType] = useState<'image' | 'video'>('image')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const serverSdk = useServerSdk()
|
||||
// 集成广告钩子
|
||||
const { showAd, loading: adLoading, adAvailable } = useAd({
|
||||
const [images, setImages] = useState<string[]>([]);
|
||||
const [mediaType, setMediaType] = useState<'image' | 'video'>('image');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const serverSdk = useServerSdk();
|
||||
const {
|
||||
showAd,
|
||||
loading: adLoading,
|
||||
adAvailable,
|
||||
} = useAd({
|
||||
onReward: () => {
|
||||
// 观看完整广告后的奖励回调
|
||||
handleDownloadImages()
|
||||
handleDownloadImages();
|
||||
},
|
||||
onClose: (isEnded) => {
|
||||
onClose: isEnded => {
|
||||
if (!isEnded) {
|
||||
showToast({
|
||||
title: '请观看完整广告才能下载',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// 获取页面参数
|
||||
const instance = getCurrentInstance()
|
||||
const params = instance?.router?.params
|
||||
const instance = getCurrentInstance();
|
||||
const params = instance?.router?.params;
|
||||
|
||||
// 兼容原有的多图片参数
|
||||
if (params?.images) {
|
||||
try {
|
||||
const imageList = JSON.parse(decodeURIComponent(params.images))
|
||||
setImages(imageList)
|
||||
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')
|
||||
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)
|
||||
console.error('解析图片参数失败:', error);
|
||||
showToast({
|
||||
title: '参数错误',
|
||||
icon: 'error'
|
||||
})
|
||||
navigateBack()
|
||||
icon: 'error',
|
||||
});
|
||||
navigateBack();
|
||||
}
|
||||
} else {
|
||||
showToast({
|
||||
title: '缺少图片参数',
|
||||
icon: 'error'
|
||||
})
|
||||
navigateBack()
|
||||
icon: 'error',
|
||||
});
|
||||
navigateBack();
|
||||
}
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
|
||||
// 检查和请求相册权限
|
||||
// const checkAndRequestAlbumPermission = async (): Promise<boolean> => {
|
||||
// try {
|
||||
// // 首先检查当前权限状态
|
||||
// const settings = await getSetting()
|
||||
// const authSetting: any = settings.authSetting
|
||||
|
||||
// // 检查不同平台的相册权限
|
||||
// const hasAlbumPermission =
|
||||
// authSetting['scope.album'] === true ||
|
||||
// authSetting['scope.writePhotosAlbum'] === true
|
||||
|
||||
// if (hasAlbumPermission) {
|
||||
// console.log('用户已授权相册权限')
|
||||
// return true
|
||||
// }
|
||||
|
||||
// // 检查是否之前被拒绝
|
||||
// const isDenied =
|
||||
// authSetting['scope.album'] === false ||
|
||||
// authSetting['scope.writePhotosAlbum'] === false
|
||||
|
||||
// if (isDenied) {
|
||||
// console.log('用户之前拒绝过权限,需要引导到设置页面')
|
||||
// throw new Error('用户之前拒绝过权限')
|
||||
// }
|
||||
|
||||
// // 用户还没有被询问过权限,尝试请求授权
|
||||
// await authorize({ scope: 'scope.writePhotosAlbum' })
|
||||
// return true
|
||||
// } catch (error) {
|
||||
// // 授权失败,询问用户是否手动开启权限
|
||||
// try {
|
||||
// const contentText = mediaType === 'video'
|
||||
// ? '需要访问您的相册权限来保存视频,请在设置中开启权限'
|
||||
// : '需要访问您的相册权限来保存图片,请在设置中开启权限'
|
||||
|
||||
// const result = await showModal({
|
||||
// title: '权限申请',
|
||||
// content: contentText,
|
||||
// confirmText: '去设置',
|
||||
// cancelText: '取消'
|
||||
// })
|
||||
|
||||
// if (result.confirm) {
|
||||
// // 用户同意去设置页面
|
||||
// const settingResult = await openSetting()
|
||||
|
||||
// // 检查用户是否在设置页面开启了权限
|
||||
// const hasPermissionAfterSetting =
|
||||
// (settingResult.authSetting as any)['scope.album'] === true ||
|
||||
// settingResult.authSetting['scope.writePhotosAlbum'] === true
|
||||
|
||||
// if (hasPermissionAfterSetting) {
|
||||
// console.log('用户在设置页面开启了权限')
|
||||
// return true
|
||||
// } else {
|
||||
// showToast({
|
||||
// title: '仍未获得权限,无法保存',
|
||||
// icon: 'none',
|
||||
// duration: 2000
|
||||
// })
|
||||
// return false
|
||||
// }
|
||||
// } else {
|
||||
// showToast({
|
||||
// title: '需要相册权限才能保存',
|
||||
// icon: 'none',
|
||||
// duration: 2000
|
||||
// })
|
||||
// return false
|
||||
// }
|
||||
// } catch (modalError) {
|
||||
// console.error('显示权限申请弹窗失败:', modalError)
|
||||
// showToast({
|
||||
// title: '权限申请失败',
|
||||
// icon: 'none',
|
||||
// duration: 2000
|
||||
// })
|
||||
// return false
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// 下载媒体文件到本地相册
|
||||
const handleDownloadImages = async () => {
|
||||
// 优先处理模板结果媒体文件
|
||||
const mediaUrl = images.length > 0 ? images[0] : ''
|
||||
if (!mediaUrl) return
|
||||
const mediaUrl = images.length > 0 ? images[0] : '';
|
||||
if (!mediaUrl) return;
|
||||
|
||||
console.log({ mediaUrl })
|
||||
console.log({ mediaUrl });
|
||||
|
||||
try {
|
||||
// 从 mediaUrl 中提取文件信息
|
||||
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() : ''
|
||||
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 }
|
||||
}
|
||||
return { fileName, extension, cleanUrl };
|
||||
};
|
||||
|
||||
const fileInfo = getFileInfoFromUrl(mediaUrl)
|
||||
const fileInfo = getFileInfoFromUrl(mediaUrl);
|
||||
|
||||
// 根据文件类型确定正确的扩展名
|
||||
let finalExtension = fileInfo.extension
|
||||
let finalExtension = fileInfo.extension;
|
||||
if (!finalExtension) {
|
||||
// 如果URL中没有扩展名,根据媒体类型推断
|
||||
finalExtension = mediaType === 'video' ? 'mp4' : 'jpg'
|
||||
finalExtension = mediaType === 'video' ? 'mp4' : 'jpg';
|
||||
}
|
||||
|
||||
// 先下载文件到临时目录,指定文件路径
|
||||
showToast({ title: '下载中...', icon: 'loading', duration: 1000 * 60 })
|
||||
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
|
||||
Accept: 'image/*, video/*',
|
||||
'User-Agent': 'Mozilla/5.0 (compatible; MiniApp)',
|
||||
},
|
||||
});
|
||||
hideToast();
|
||||
const filePath = downloadRes.tempFilePath;
|
||||
|
||||
showToast({ title: '保存中...', icon: 'loading' })
|
||||
// 根据实际文件类型选择保存方法
|
||||
showToast({ title: '保存中...', icon: 'loading' });
|
||||
if (mediaType === 'video') {
|
||||
console.log('保存为视频文件')
|
||||
await saveVideoToPhotosAlbum({ filePath })
|
||||
console.log('保存为视频文件');
|
||||
await saveVideoToPhotosAlbum({ filePath });
|
||||
} else if (mediaType === 'image') {
|
||||
console.log('保存为图片文件')
|
||||
await saveImageToPhotosAlbum({ filePath })
|
||||
console.log('保存为图片文件');
|
||||
await saveImageToPhotosAlbum({ filePath });
|
||||
} else {
|
||||
// 如果无法确定文件类型,默认尝试保存为图片
|
||||
console.log('文件类型不确定,尝试保存为图片')
|
||||
console.log('文件类型不确定,尝试保存为图片');
|
||||
try {
|
||||
await saveImageToPhotosAlbum({ filePath })
|
||||
await saveImageToPhotosAlbum({ filePath });
|
||||
} catch (imageError) {
|
||||
console.log('图片保存失败,尝试视频保存')
|
||||
await saveVideoToPhotosAlbum({ filePath })
|
||||
console.log('图片保存失败,尝试视频保存');
|
||||
await saveVideoToPhotosAlbum({ filePath });
|
||||
}
|
||||
}
|
||||
|
||||
showToast({
|
||||
title: '保存成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
duration: 2000,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
console.error('保存失败:', error);
|
||||
|
||||
// 分析错误类型,提供更具体的错误提示
|
||||
let errorMsg = '保存失败,请重试'
|
||||
let errorMsg = '保存失败,请重试';
|
||||
if (error && typeof error === 'object') {
|
||||
const err = error as any
|
||||
const err = error as any;
|
||||
if (err.errMsg) {
|
||||
console.error('详细错误信息:', err.errMsg)
|
||||
console.error('详细错误信息:', err.errMsg);
|
||||
if (err.errMsg.includes('auth')) {
|
||||
errorMsg = '权限不足,请在设置中开启相册权限'
|
||||
errorMsg = '权限不足,请在设置中开启相册权限';
|
||||
} else if (err.errMsg.includes('network')) {
|
||||
errorMsg = '网络错误,请检查网络连接'
|
||||
errorMsg = '网络错误,请检查网络连接';
|
||||
} else if (err.errMsg.includes('invalid file type')) {
|
||||
errorMsg = '文件格式不支持,请联系客服'
|
||||
errorMsg = '文件格式不支持,请联系客服';
|
||||
} else if (err.errMsg.includes('file')) {
|
||||
errorMsg = '文件格式不支持或已损坏'
|
||||
errorMsg = '文件格式不支持或已损坏';
|
||||
} else if (err.errMsg.includes('download')) {
|
||||
errorMsg = '文件下载失败,请重试'
|
||||
errorMsg = '文件下载失败,请重试';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -241,92 +151,79 @@ const ResultPage: React.FC = () => {
|
||||
showToast({
|
||||
title: errorMsg,
|
||||
icon: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
duration: 3000,
|
||||
});
|
||||
} finally {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 再来一张
|
||||
const handleRegenerate = () => {
|
||||
navigateBack()
|
||||
}
|
||||
navigateBack();
|
||||
};
|
||||
|
||||
// 图片生视频
|
||||
const handleImageToVideo = async () => {
|
||||
if (mediaType !== 'image' || !images[0]) {
|
||||
showToast({
|
||||
title: '仅支持图片生成视频',
|
||||
icon: 'error',
|
||||
duration: 2000
|
||||
})
|
||||
return
|
||||
duration: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
setLoading(true);
|
||||
showToast({
|
||||
title: '正在生成视频...',
|
||||
icon: 'loading',
|
||||
duration: 60000
|
||||
})
|
||||
duration: 60000,
|
||||
});
|
||||
|
||||
const taskId = await serverSdk.executeTemplate({
|
||||
imageUrl: images[0],
|
||||
templateCode: 'character_figurine_v1'
|
||||
})
|
||||
templateCode: 'character_figurine_v1',
|
||||
});
|
||||
|
||||
hideToast()
|
||||
hideToast();
|
||||
|
||||
// 跳转到生成页面
|
||||
navigateTo({
|
||||
url: `/pages/generate/index?taskId=${taskId}&templateCode=character_figurine_v1`
|
||||
})
|
||||
url: `/pages/generate/index?taskId=${taskId}&templateCode=character_figurine_v1`,
|
||||
});
|
||||
} catch (error) {
|
||||
hideToast()
|
||||
console.error('图片生视频失败:', error)
|
||||
hideToast();
|
||||
console.error('图片生视频失败:', error);
|
||||
showToast({
|
||||
title: '生成失败,请重试',
|
||||
icon: 'error',
|
||||
duration: 2000
|
||||
})
|
||||
duration: 2000,
|
||||
});
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 兼容原有的多图片显示
|
||||
if (!images.length) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='result-page'>
|
||||
<View className="result-page">
|
||||
{mediaType === 'video' ? (
|
||||
<View className='result-video-container'>
|
||||
<Video
|
||||
className='result-video'
|
||||
src={images[0]}
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
controls
|
||||
objectFit='contain'
|
||||
/>
|
||||
<View className="result-video-container">
|
||||
<Video className="result-video" src={images[0]} autoplay controls={false} muted loop objectFit="contain" />
|
||||
</View>
|
||||
) : (
|
||||
<View
|
||||
className='result-image-container'
|
||||
className="result-image-container"
|
||||
style={{
|
||||
backgroundImage: `url(${images[0]})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat'
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}}
|
||||
>
|
||||
<View className='backdrop-blur'>
|
||||
<Image className='result-image' src={images[0]} mode='aspectFit' />
|
||||
<View className="backdrop-blur">
|
||||
<Image className="result-image" src={images[0]} mode="aspectFit" />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
@@ -340,7 +237,7 @@ const ResultPage: React.FC = () => {
|
||||
imageToVideoLoading={loading}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default ResultPage
|
||||
export default ResultPage;
|
||||
|
||||
Reference in New Issue
Block a user