style: 统一样式单位为px,优化代码格式和可读性

- 将CSS样式中的rpx单位替换为px,提升样式一致性
- 在TypeScript文件中添加缺失的分号,优化代码可读性
- 删除不再使用的组件及其样式,简化项目结构
- 更新生成页面和结果页面的样式,提升用户体验
This commit is contained in:
iHeyTang
2025-09-10 20:12:50 +08:00
parent 09aa9d6192
commit b3c7c09e13
16 changed files with 456 additions and 1251 deletions

View File

@@ -5,22 +5,19 @@
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding-left: 0; padding: 20px 0 40px;
padding-right: 0;
padding-bottom: 40rpx;
padding-top: 20rpx;
} }
.download-btn { .download-btn {
width: 80%; width: 80%;
height: 100rpx; height: 100px;
background: linear-gradient(45deg, #ff6b6b, #ee5a24); background: linear-gradient(45deg, #ff6b6b, #ee5a24);
color: white; color: white;
border: none; border: none;
border-radius: 50rpx; border-radius: 50px;
font-size: 32rpx; font-size: 32px;
font-weight: bold; font-weight: bold;
margin-bottom: 20rpx; margin-bottom: 20px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -52,14 +49,14 @@
.regenerate-btn { .regenerate-btn {
width: 80%; width: 80%;
height: 100rpx; height: 100px;
background: linear-gradient(45deg, #52c41a, #73d13d); background: linear-gradient(45deg, #52c41a, #73d13d);
color: white; color: white;
border: none; border: none;
border-radius: 50rpx; border-radius: 50px;
font-size: 32rpx; font-size: 32px;
font-weight: bold; font-weight: bold;
margin-bottom: 20rpx; margin-bottom: 20px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -72,6 +69,6 @@
} }
.download-tip { .download-tip {
font-size: 24rpx; font-size: 24px;
opacity: 0.8; opacity: 0.8;
} }

View File

@@ -1,67 +1,61 @@
import { View, Text } from '@tarojs/components' import { View, Text } from '@tarojs/components';
import './index.css' import './index.css';
interface DownloadSectionProps { interface DownloadSectionProps {
onDownload: () => void onDownload: () => void;
onRegenerate?: () => void onRegenerate?: () => void;
onImageToVideo?: () => void onImageToVideo?: () => void;
loading: boolean loading: boolean;
hasWatchedAd?: boolean hasWatchedAd?: boolean;
adAvailable?: boolean adAvailable?: boolean;
showImageToVideo?: boolean showImageToVideo?: boolean;
imageToVideoLoading?: boolean imageToVideoLoading?: boolean;
} }
const DownloadSection: React.FC<DownloadSectionProps> = ({ const DownloadSection: React.FC<DownloadSectionProps> = ({
onDownload, onDownload,
onRegenerate, onRegenerate,
onImageToVideo, onImageToVideo,
loading, loading,
adAvailable = true, adAvailable = true,
showImageToVideo = false, showImageToVideo = false,
imageToVideoLoading = false imageToVideoLoading = false,
}) => { }) => {
const getButtonText = () => { const getButtonText = () => {
if (loading) { if (loading) {
return '广告加载中...' return '广告加载中...';
} }
if (!adAvailable) { if (!adAvailable) {
return '📱 免费下载' return '📱 免费下载';
} }
return '🎬 看广告下载' return '🎬 看广告下载';
} };
const getTipText = () => { const getTipText = () => {
if (!adAvailable) { if (!adAvailable) {
return '🎁 已为您跳过广告,可以直接免费下载' return '🎁 已为您跳过广告,可以直接免费下载';
} }
return '观看完整广告即可免费下载所有图片' return '观看完整广告即可免费下载所有图片';
} };
return ( return (
<View className='download-section'> <View className="download-section">
<View <View className={`download-btn ${loading ? 'disabled' : ''} ${!adAvailable ? 'watched' : ''}`} onClick={loading ? undefined : onDownload}>
className={`download-btn ${loading ? 'disabled' : ''} ${!adAvailable ? 'watched' : ''}`}
onClick={loading ? undefined : onDownload}
>
<Text>{getButtonText()}</Text> <Text>{getButtonText()}</Text>
</View> </View>
{onRegenerate && ( {onRegenerate && (
<View className='regenerate-btn' onClick={onRegenerate}> <View className="regenerate-btn" onClick={onRegenerate}>
<Text>🎨 </Text> <Text>🎨 </Text>
</View> </View>
)} )}
{showImageToVideo && onImageToVideo && ( {showImageToVideo && onImageToVideo && (
<View <View className={`regenerate-btn ${imageToVideoLoading ? 'disabled' : ''}`} onClick={imageToVideoLoading ? undefined : onImageToVideo}>
className={`regenerate-btn ${imageToVideoLoading ? 'disabled' : ''}`}
onClick={imageToVideoLoading ? undefined : onImageToVideo}
>
<Text>{imageToVideoLoading ? '🎬 生成中...' : '🎬 图片生视频'}</Text> <Text>{imageToVideoLoading ? '🎬 生成中...' : '🎬 图片生视频'}</Text>
</View> </View>
)} )}
<Text className='download-tip'>{getTipText()}</Text> <Text className="download-tip">{getTipText()}</Text>
</View> </View>
) );
} };
export default DownloadSection export default DownloadSection;

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: rgba(0, 0, 0, 0.8); background: rgb(0 0 0 / 80%);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@@ -19,53 +19,58 @@
.loading-text { .loading-text {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20rpx; gap: 20px;
} }
.loading-spinner { .loading-spinner {
width: 100rpx; width: 100px;
height: 100rpx; height: 100px;
border: 6rpx solid #f3f3f3; border: 6px solid #f3f3f3;
border-top: 6rpx solid #667eea; border-top: 6px solid #667eea;
border-radius: 50%; border-radius: 50%;
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
margin: 0 auto 40rpx; margin: 0 auto 40px;
} }
@keyframes spin { @keyframes spin {
0% { transform: rotate(0deg); } 0% {
100% { transform: rotate(360deg); } transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
} }
.loading-title { .loading-title {
font-size: 36rpx; font-size: 36px;
font-weight: bold; font-weight: bold;
display: block; display: block;
} }
.loading-desc { .loading-desc {
font-size: 28rpx; font-size: 28px;
opacity: 0.8; opacity: 0.8;
display: block; display: block;
} }
.loading-container .action-buttons { .loading-container .action-buttons {
margin-top: 60rpx; margin-top: 60px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20rpx; gap: 20px;
width: 240rpx; width: 240px;
align-items: center; align-items: center;
} }
.loading-container .btn-primary, .loading-container .btn-primary,
.loading-container .btn-secondary { .loading-container .btn-secondary {
border-radius: 25rpx; border-radius: 25px;
height: 80rpx; height: 80px;
line-height: 80rpx; line-height: 80px;
font-size: 28rpx; font-size: 28px;
border: none; border: none;
width: 240rpx; width: 240px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -77,6 +82,6 @@
} }
.loading-container .btn-secondary { .loading-container .btn-secondary {
background: rgba(255, 255, 255, 0.9); background: rgb(255 255 255 / 90%);
color: black; color: black;
} }

View File

@@ -1,23 +1,23 @@
import { View, Text } from '@tarojs/components' import { View, Text } from '@tarojs/components';
import './index.css' import './index.css';
interface LoadingOverlayProps { interface LoadingOverlayProps {
children?: React.ReactNode children?: React.ReactNode;
} }
const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ children }) => { const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ children }) => {
return ( return (
<View className='loading-overlay'> <View className="loading-overlay">
<View className='loading-container'> <View className="loading-container">
<View className='loading-spinner' /> <View className="loading-spinner" />
<View className='loading-text'> <View className="loading-text">
<Text className='loading-title'>AI正在生成中...</Text> <Text className="loading-title">AI正在生成中...</Text>
<Text className='loading-desc'></Text> <Text className="loading-desc"></Text>
</View> </View>
{children} {children}
</View> </View>
</View> </View>
) );
} };
export default LoadingOverlay export default LoadingOverlay;

View File

@@ -1,8 +1,8 @@
.template-card { .template-card {
background: #fff; background: #fff;
border-radius: 32rpx; border-radius: 32px;
padding: 0; 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; cursor: pointer;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -15,7 +15,6 @@
will-change: transform; will-change: transform;
} }
/* 优化简化active状态减少重绘 */
.template-card:active { .template-card:active {
opacity: 0.9; opacity: 0.9;
} }
@@ -25,17 +24,17 @@
position: relative; position: relative;
padding: 0; padding: 0;
flex: 1; flex: 1;
min-height: 480rpx; min-height: 480px;
border-radius: 32rpx; border-radius: 32px;
} }
.merged-image-container { .merged-image-container {
position: relative; position: relative;
border-radius: 32rpx; border-radius: 32px;
overflow: hidden; overflow: hidden;
width: 100%; width: 100%;
height: 100%; height: 100%;
min-height: 480rpx; min-height: 480px;
display: block; display: block;
} }
@@ -45,13 +44,11 @@
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
min-height: 480rpx; min-height: 480px;
} }
/* 优化减少clip-path动画频率提升性能 */
.overlay-layer { .overlay-layer {
transition: clip-path 0.2s ease-out; transition: clip-path 0.2s ease-out;
/* 启用硬件加速 */
transform: translateZ(0); transform: translateZ(0);
will-change: clip-path; will-change: clip-path;
} }
@@ -59,28 +56,21 @@
.full-image { .full-image {
width: 100%; width: 100%;
height: 100%; height: 100%;
min-height: 480rpx; min-height: 480px;
object-fit: cover; object-fit: cover;
display: block; display: block;
/* 性能优化:启用硬件加速和图片优化 */
transform: translateZ(0); transform: translateZ(0);
image-rendering: optimizeSpeed; image-rendering: optimizespeed;
/* 防止图片闪烁 */
backface-visibility: hidden; backface-visibility: hidden;
} }
/* 可拖拽分割线 */ /* 可拖拽分割线 */
.split-line { .split-line {
position: absolute; position: absolute;
top: 0; top: 0;
bottom: 0; bottom: 0;
width: 8rpx; /* 增加宽度便于拖拽 */ width: 8px; /* 增加宽度便于拖拽 */
background: linear-gradient(to bottom, background: linear-gradient(to bottom, rgb(255 255 255 / 80%) 0%, rgb(255 255 255 / 90%) 50%, rgb(255 255 255 / 80%) 100%);
rgba(255, 255, 255, 0.8) 0%,
rgba(255, 255, 255, 0.9) 50%,
rgba(255, 255, 255, 0.8) 100%);
transform: translateX(-50%); transform: translateX(-50%);
z-index: 3; z-index: 3;
cursor: col-resize; cursor: col-resize;
@@ -93,15 +83,15 @@
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
width: 64rpx; width: 64px;
height: 64rpx; height: 64px;
background: rgba(255, 255, 255, 0.95); background: rgb(255 255 255 / 95%);
border-radius: 50%; border-radius: 50%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.25); box-shadow: 0 4px 24px rgb(0 0 0 / 25%);
border: 4rpx solid rgba(0, 122, 255, 0.4); border: 4px solid rgb(0 122 255 / 40%);
cursor: col-resize; cursor: col-resize;
touch-action: none; touch-action: none;
transition: all 0.2s ease; transition: all 0.2s ease;
@@ -109,12 +99,12 @@
.split-handle:active { .split-handle:active {
transform: translate(-50%, -50%) scale(1.1); 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 { .split-icon {
font-size: 24rpx; font-size: 24px;
color: #007AFF; color: #007aff;
font-weight: bold; font-weight: bold;
} }
@@ -123,9 +113,9 @@
/* 模板名称悬浮 - 图片底部 */ /* 模板名称悬浮 - 图片底部 */
.name-overlay { .name-overlay {
position: absolute; position: absolute;
bottom: 12rpx; bottom: 12px;
left: 24rpx; left: 24px;
right: 24rpx; right: 24px;
z-index: 4; z-index: 4;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -134,15 +124,14 @@
} }
.name-badge { .name-badge {
background: rgba(0, 0, 0, 0.7); background: rgb(0 0 0 / 70%);
backdrop-filter: blur(20rpx); backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20rpx);
color: #fff; color: #fff;
font-size: 24rpx; font-size: 24px;
font-weight: 600; font-weight: 600;
padding: 8rpx 20rpx; padding: 8px 20px;
border-radius: 20rpx; border-radius: 20px;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3); text-shadow: 0 2px 4px rgb(0 0 0 / 30%);
text-align: center; text-align: center;
max-width: 90%; max-width: 90%;
width: fit-content; width: fit-content;
@@ -154,25 +143,23 @@
/* 单视频容器样式 */ /* 单视频容器样式 */
.single-video-container { .single-video-container {
position: relative; position: relative;
border-radius: 32rpx; border-radius: 32px;
overflow: hidden; overflow: hidden;
width: 100%; width: 100%;
min-height: 480rpx; min-height: 480px;
display: block; display: block;
} }
.single-video { .single-video {
width: 100%; width: 100%;
height: 100%; height: 100%;
min-height: 480rpx; min-height: 480px;
object-fit: cover; object-fit: cover;
display: block; display: block;
border-radius: 32rpx; border-radius: 32px;
position: relative; position: relative;
z-index: 2; z-index: 2;
/* 性能优化:启用硬件加速 */
transform: translateZ(0); transform: translateZ(0);
/* 防止视频闪烁 */
backface-visibility: hidden; backface-visibility: hidden;
} }
@@ -185,27 +172,23 @@
height: 100%; height: 100%;
z-index: 1; z-index: 1;
object-fit: cover; object-fit: cover;
border-radius: 32rpx; border-radius: 32px;
} }
/* 视频样式 */ /* 视频样式 */
.full-video { .full-video {
width: 100%; width: 100%;
height: 100%; height: 100%;
min-height: 480rpx; min-height: 480px;
object-fit: cover; object-fit: cover;
display: block; display: block;
/* 性能优化:启用硬件加速 */
transform: translateZ(0); transform: translateZ(0);
/* 防止视频闪烁 */
backface-visibility: hidden; backface-visibility: hidden;
} }
.watermark-text { .watermark-text {
color: rgba(255, 255, 255, 0.5); color: rgb(255 255 255 / 50%);
font-size: 20rpx; font-size: 20px;
font-weight: 400; font-weight: 400;
text-align: center; text-align: center;
} }

View File

@@ -1,186 +1,165 @@
import { View, Text, Image, Video } from '@tarojs/components' import { View, Text, Image, Video } from '@tarojs/components';
import { useState, useRef, useMemo } from 'react' import { useState, useRef, useMemo } from 'react';
import Taro from '@tarojs/taro' import Taro from '@tarojs/taro';
import { Template } from '../../store/types' import { Template } from '../../store/types';
import './index.css' import './index.css';
interface TemplateCardProps { interface TemplateCardProps {
template: Template template: Template;
onClick: (template: Template) => void onClick: (template: Template) => void;
} }
export default function TemplateCard({ template, onClick }: TemplateCardProps) { export default function TemplateCard({ template, onClick }: TemplateCardProps) {
const [splitPosition, setSplitPosition] = useState(50) // 分割线位置百分比 const [splitPosition, setSplitPosition] = useState(50); // 分割线位置百分比
const [isDragging, setIsDragging] = useState(false) const [isDragging, setIsDragging] = useState(false);
const [containerInfo, setContainerInfo] = useState<any>(null) const [containerInfo, setContainerInfo] = useState<any>(null);
const containerRef = useRef<any>(null) const containerRef = useRef<any>(null);
const containerId = `container-${template.code}` // 为每个容器创建唯一ID const containerId = `container-${template.code}`; // 为每个容器创建唯一ID
// 检测output是否为视频 // 检测output是否为视频
const isOutputVideo = useMemo(() => { const isOutputVideo = useMemo(() => {
return /\.(mp4|webm|ogg|mov|avi|mkv|flv)$/i.test(template.outputExample) return /\.(mp4|webm|ogg|mov|avi|mkv|flv)$/i.test(template.outputExample);
}, [template.outputExample]) }, [template.outputExample]);
// 检测input是否为视频 // 检测input是否为视频
const isInputVideo = useMemo(() => { const isInputVideo = useMemo(() => {
return /\.(mp4|webm|ogg|mov|avi|mkv|flv)$/i.test(template.inputExample) return /\.(mp4|webm|ogg|mov|avi|mkv|flv)$/i.test(template.inputExample);
}, [template.inputExample]) }, [template.inputExample]);
const handleClick = () => { const handleClick = () => {
if (!isDragging) { if (!isDragging) {
onClick(template) onClick(template);
} }
} };
// 获取容器信息 // 获取容器信息
const getContainerInfo = () => { const getContainerInfo = () => {
return new Promise((resolve) => { return new Promise(resolve => {
const query = Taro.createSelectorQuery() const query = Taro.createSelectorQuery();
query.select(`#${containerId}`).boundingClientRect((rect) => { query
if (rect) { .select(`#${containerId}`)
setContainerInfo(rect) .boundingClientRect(rect => {
resolve(rect) if (rect) {
} setContainerInfo(rect);
}).exec() resolve(rect);
}) }
} })
.exec();
});
};
// 处理触摸开始 // 处理触摸开始
const handleTouchStart = async (e: any) => { const handleTouchStart = async (e: any) => {
e.stopPropagation() e.stopPropagation();
setIsDragging(true) setIsDragging(true);
// 获取容器信息用于后续计算 // 获取容器信息用于后续计算
await getContainerInfo() await getContainerInfo();
} };
// 处理触摸移动 - 优化:添加节流,减少计算频率 // 处理触摸移动 - 优化:添加节流,减少计算频率
const handleTouchMove = (e: any) => { const handleTouchMove = (e: any) => {
if (!isDragging || !containerInfo) return if (!isDragging || !containerInfo) return;
e.stopPropagation() e.stopPropagation();
const touch = e.touches[0] const touch = e.touches[0];
if (!touch) return if (!touch) return;
// 使用requestAnimationFrame节流提升性能 // 使用requestAnimationFrame节流提升性能
requestAnimationFrame(() => { requestAnimationFrame(() => {
// 计算触摸点相对于容器的位置 // 计算触摸点相对于容器的位置
const touchX = touch.clientX - containerInfo.left const touchX = touch.clientX - containerInfo.left;
const percentage = Math.max(10, Math.min(90, (touchX / containerInfo.width) * 100)) const percentage = Math.max(10, Math.min(90, (touchX / containerInfo.width) * 100));
setSplitPosition(percentage) setSplitPosition(percentage);
}) });
} };
// 处理触摸结束 // 处理触摸结束
const handleTouchEnd = (e: any) => { const handleTouchEnd = (e: any) => {
e.stopPropagation() e.stopPropagation();
setTimeout(() => setIsDragging(false), 100) // 延迟重置,避免触发点击 setTimeout(() => setIsDragging(false), 100); // 延迟重置,避免触发点击
} };
return ( return (
<View className='template-card' onClick={handleClick}> <View className="template-card" onClick={handleClick}>
{/* 根据output类型显示不同的内容 */} {/* 根据output类型显示不同的内容 */}
{isOutputVideo ? ( {isOutputVideo ? (
// 当output是视频时只显示单个视频 // 当output是视频时只显示单个视频
<View className='single-video-container'> <View className="single-video-container">
<Image <Image className="video-poster" src={template.inputExample} mode="aspectFill" />
className='video-poster'
src={template.inputExample}
mode='aspectFill'
/>
<Video <Video
className='single-video' className="single-video"
src={template.outputExample} src={template.outputExample}
autoplay autoplay
muted muted
loop loop
objectFit='cover' objectFit="cover"
showPlayBtn={false} showPlayBtn={false}
showCenterPlayBtn={false} showCenterPlayBtn={false}
showFullscreenBtn={false} showFullscreenBtn={false}
controls={false} controls={false}
/> />
{/* 模板名称悬浮 - 视频底部 */} {/* 模板名称悬浮 - 视频底部 */}
<View className='name-overlay'> <View className="name-overlay">
<Text className='name-badge'> <Text className="name-badge">{template.name}</Text>
{template.name} <Text className="watermark-text">AI生成</Text>
</Text>
<Text className='watermark-text'>AI生成</Text>
</View> </View>
</View> </View>
) : ( ) : (
// 原有的图片对比逻辑 // 原有的图片对比逻辑
<View className='image-comparison'> <View className="image-comparison">
<View <View id={containerId} className="merged-image-container" ref={containerRef} onTouchMove={handleTouchMove} onTouchEnd={handleTouchEnd}>
id={containerId}
className='merged-image-container'
ref={containerRef}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
{/* 原图层 - 完整图片/视频 */} {/* 原图层 - 完整图片/视频 */}
<View className='image-layer'> <View className="image-layer">
{isInputVideo ? ( {isInputVideo ? (
<Video <Video
className='full-video' className="full-video"
src={template.inputExample} src={template.inputExample}
autoplay autoplay
muted muted
loop loop
objectFit='cover' objectFit="cover"
showPlayBtn={false} showPlayBtn={false}
showCenterPlayBtn={false} showCenterPlayBtn={false}
showFullscreenBtn={false} showFullscreenBtn={false}
controls={false} controls={false}
/> />
) : ( ) : (
<Image <Image className="full-image" src={template.inputExample} mode="aspectFill" lazyLoad />
className='full-image'
src={template.inputExample}
mode='aspectFill'
lazyLoad
/>
)} )}
</View> </View>
{/* 效果图层 - 完整图片,通过遮罩显示右半部分 */} {/* 效果图层 - 完整图片,通过遮罩显示右半部分 */}
<View <View
className='image-layer overlay-layer' className="image-layer overlay-layer"
style={{ style={{
clipPath: `polygon(${splitPosition}% 0%, 100% 0%, 100% 100%, ${splitPosition}% 100%)` clipPath: `polygon(${splitPosition}% 0%, 100% 0%, 100% 100%, ${splitPosition}% 100%)`,
}} }}
> >
<Image <Image className="full-image" src={template.outputExample} mode="aspectFill" lazyLoad />
className='full-image'
src={template.outputExample}
mode='aspectFill'
lazyLoad
/>
</View> </View>
{/* 可拖拽的分割线 */} {/* 可拖拽的分割线 */}
<View <View
className='split-line' className="split-line"
style={{ left: `${splitPosition}%` }} style={{ left: `${splitPosition}%` }}
onTouchStart={handleTouchStart} onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove} onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd} onTouchEnd={handleTouchEnd}
> >
<View className='split-handle'> <View className="split-handle">
<Text className='split-icon'></Text> <Text className="split-icon"></Text>
</View> </View>
</View> </View>
{/* 模板名称悬浮 - 图片底部 */} {/* 模板名称悬浮 - 图片底部 */}
<View className='name-overlay'> <View className="name-overlay">
<Text className='name-badge'> <Text className="name-badge">{template.name}</Text>
{template.name} <Text className="watermark-text">AI生成</Text>
</Text>
<Text className='watermark-text'>AI生成</Text>
</View> </View>
</View> </View>
</View> </View>
)} )}
</View> </View>
) );
} }

View File

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

View File

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

View File

@@ -10,39 +10,39 @@
.generate-loading { .generate-loading {
text-align: center; text-align: center;
padding: 60rpx; padding: 30px;
} }
.progress-container { .progress-container {
margin-bottom: 40rpx; margin-bottom: 20px;
} }
.progress-bar { .progress-bar {
width: 500rpx; width: 250px;
height: 12rpx; height: 6px;
background-color: #e9ecef; background-color: #e9ecef;
border-radius: 6rpx; border-radius: 3px;
overflow: hidden; overflow: hidden;
margin-bottom: 20rpx; margin-bottom: 10px;
} }
.progress-fill { .progress-fill {
height: 100%; height: 100%;
background: linear-gradient(90deg, #007aff 0%, #34c759 100%); background: linear-gradient(90deg, #007aff 0%, #34c759 100%);
border-radius: 6rpx; border-radius: 3px;
transition: width 0.3s ease; transition: width 0.3s ease;
} }
.progress-text { .progress-text {
font-size: 28rpx; font-size: 14px;
color: #666; color: #666;
font-weight: 500; font-weight: 500;
} }
.loading-text { .loading-text {
font-size: 32rpx; font-size: 16px;
color: #333; color: #333;
margin-top: 20rpx; margin-top: 10px;
} }
.generate-success { .generate-success {
@@ -52,42 +52,41 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%); background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
padding: 40rpx 32rpx; padding: 20px 16px;
box-sizing: border-box; box-sizing: border-box;
} }
/* 成功页面头部 */ /* 成功页面头部 */
.success-header { .success-header {
text-align: center; text-align: center;
margin-bottom: 40rpx; margin-bottom: 20px;
padding-top: 80rpx; padding-top: 40px;
} }
.success-badge { .success-badge {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 12rpx; gap: 6px;
margin-bottom: 24rpx; margin-bottom: 12px;
} }
.success-icon { .success-icon {
font-size: 64rpx; font-size: 32px;
filter: drop-shadow(0 4rpx 8rpx rgba(0, 0, 0, 0.1)); filter: drop-shadow(0 2px 4px rgb(0 0 0 / 10%));
} }
.success-title { .success-title {
font-size: 48rpx; font-size: 24px;
font-weight: 700; font-weight: 700;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
letter-spacing: -0.5rpx; letter-spacing: -0.25px;
} }
.success-subtitle { .success-subtitle {
font-size: 28rpx; font-size: 14px;
color: #666; color: #666;
font-weight: 500; font-weight: 500;
line-height: 1.5; line-height: 1.5;
@@ -99,21 +98,21 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
margin-bottom: 40rpx; margin-bottom: 20px;
min-height: 0; min-height: 0;
padding: 0 16rpx; padding: 0 8px;
} }
.preview-container { .preview-container {
border-radius: 32rpx; border-radius: 16px;
padding: 0; padding: 0;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1); box-shadow: 0 4px 16px rgb(0 0 0 / 10%);
border: 1rpx solid #f0f0f0; border: 0.5px solid #f0f0f0;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
width: 90%; width: 90%;
max-width: 600rpx; max-width: 300px;
min-height: 680rpx; min-height: 340px;
height: 60vh; height: 60vh;
display: block; display: block;
} }
@@ -121,22 +120,22 @@
.preview-image { .preview-image {
width: 100%; width: 100%;
height: 100%; height: 100%;
min-height: 680rpx; min-height: 340px;
object-fit: cover; object-fit: cover;
border-radius: 32rpx; border-radius: 16px;
cursor: pointer; cursor: pointer;
display: block; display: block;
transform: translateZ(0); transform: translateZ(0);
image-rendering: optimizeSpeed; image-rendering: optimizespeed;
backface-visibility: hidden; backface-visibility: hidden;
} }
.preview-video { .preview-video {
width: 100%; width: 100%;
height: 100%; height: 100%;
min-height: 680rpx; min-height: 340px;
object-fit: cover; object-fit: cover;
border-radius: 32rpx; border-radius: 16px;
display: block; display: block;
transform: translateZ(0); transform: translateZ(0);
backface-visibility: hidden; backface-visibility: hidden;
@@ -170,7 +169,7 @@
.generate-error { .generate-error {
text-align: center; text-align: center;
padding: 60rpx; padding: 30px;
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%); background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
@@ -180,33 +179,33 @@
} }
.error-container { .error-container {
margin-bottom: 60rpx; margin-bottom: 30px;
background: #fff; background: #fff;
border-radius: 32rpx; border-radius: 16px;
padding: 60rpx 40rpx; padding: 30px 20px;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1); box-shadow: 0 4px 16px rgb(0 0 0 / 10%);
border: 1rpx solid #f0f0f0; border: 0.5px solid #f0f0f0;
max-width: 600rpx; max-width: 300px;
} }
.error-text { .error-text {
font-size: 40rpx; font-size: 20px;
color: #ff3b30; color: #ff3b30;
font-weight: 700; font-weight: 700;
margin-bottom: 20rpx; margin-bottom: 10px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 12rpx; gap: 6px;
} }
.error-text::before { .error-text::before {
content: '⚠️'; content: '⚠️';
font-size: 36rpx; font-size: 18px;
} }
.error-message { .error-message {
font-size: 30rpx; font-size: 15px;
color: #666; color: #666;
line-height: 1.4; line-height: 1.4;
} }
@@ -214,44 +213,43 @@
.action-buttons { .action-buttons {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 24rpx; gap: 12px;
width: 100%; width: 100%;
max-width: 600rpx; max-width: 300px;
margin: 0 auto 32rpx; margin: 0 auto 16px;
} }
.success-actions .action-buttons { .success-actions .action-buttons {
margin-bottom: 24rpx; margin-bottom: 12px;
} }
/* 结果提示 */ /* 结果提示 */
.result-tips { .result-tips {
text-align: center; text-align: center;
margin-top: 16rpx; margin-top: 8px;
} }
.tips-text { .tips-text {
font-size: 24rpx; font-size: 12px;
color: #999; color: #999;
background: rgba(255, 255, 255, 0.8); background: rgb(255 255 255 / 80%);
padding: 12rpx 24rpx; padding: 6px 12px;
border-radius: 20rpx; border-radius: 10px;
display: inline-block; display: inline-block;
backdrop-filter: blur(10rpx); backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(10rpx); box-shadow: 0 1px 4px rgb(0 0 0 / 5%);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
} }
.btn-primary { .btn-primary {
flex: 1; flex: 1;
background: linear-gradient(135deg, #FFD67A 0%, #FFC947 100%); background: linear-gradient(135deg, #ffd67a 0%, #ffc947 100%);
color: #333; color: #333;
border-radius: 24rpx; border-radius: 12px;
height: 96rpx; height: 48px;
font-size: 32rpx; font-size: 16px;
font-weight: 600; font-weight: 600;
border: none; 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; position: relative;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
@@ -266,7 +264,7 @@
left: -100%; left: -100%;
width: 100%; width: 100%;
height: 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; z-index: 1;
} }
@@ -276,16 +274,15 @@
.btn-secondary { .btn-secondary {
flex: 1; flex: 1;
background: rgba(255, 255, 255, 0.9); background: rgb(255 255 255 / 90%);
color: #333; color: #333;
border: 2rpx solid rgba(255, 214, 122, 0.6); border: 1px solid rgb(255 214 122 / 60%);
border-radius: 24rpx; border-radius: 12px;
height: 96rpx; height: 48px;
font-size: 32rpx; font-size: 16px;
font-weight: 600; font-weight: 600;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08); box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
backdrop-filter: blur(10rpx); backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(10rpx);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -298,4 +295,4 @@
.btn-text { .btn-text {
position: relative; position: relative;
z-index: 2; z-index: 2;
} }

View File

@@ -1,127 +1,121 @@
import { View, Text, Button } from '@tarojs/components' import { Button, Text, View } from '@tarojs/components';
import { useEffect, useState } from 'react' import Taro, { navigateTo, switchTab } from '@tarojs/taro';
import Taro, { switchTab, navigateTo } from '@tarojs/taro' import { useEffect, useState } from 'react';
import { useServerSdk } from '../../hooks/index' import LoadingOverlay from '../../components/LoadingOverlay';
import LoadingOverlay from '../../components/LoadingOverlay' import { useServerSdk } from '../../hooks/index';
import './index.css' import './index.css';
export default function Generate() { export default function Generate() {
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading') const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
const [result, setResult] = useState<any | null>(null) const [result, setResult] = useState<any | null>(null);
const serverSdk = useServerSdk() const serverSdk = useServerSdk();
let timerRef: NodeJS.Timeout | null = null let timerRef: NodeJS.Timeout | null = null;
useEffect(() => { useEffect(() => {
// 获取页面参数 const instance = Taro.getCurrentInstance();
const instance = Taro.getCurrentInstance() const params = instance.router?.params;
const params = instance.router?.params
if (params?.taskId) { if (params?.taskId) {
pollTaskResult(params.taskId) pollTaskResult(params.taskId);
} else { } else {
setStatus('error') setStatus('error');
setResult({ success: false, error: '缺少必要参数' }) setResult({ success: false, error: '缺少必要参数' });
} }
// 页面离开时清理定时器
return () => { return () => {
if (timerRef) { if (timerRef) {
clearTimeout(timerRef) clearTimeout(timerRef);
timerRef = null timerRef = null;
} }
} };
}, []) }, []);
const pollTaskResult = async (taskId: string) => { const pollTaskResult = async (taskId: string) => {
try { try {
let attempts = 0 let attempts = 0;
const maxAttempts = 30 * 20 // 最多轮询30次30秒 const maxAttempts = 30 * 20;
const poll = async () => { const poll = async () => {
attempts++ attempts++;
try { try {
const result = await serverSdk.getTaskProgress(taskId) const result = await serverSdk.getTaskProgress(taskId);
if (result.status === 'completed') { if (result.status === 'completed') {
setStatus('success') setStatus('success');
navigateTo({ navigateTo({
url: `/pages/result/index?images=${encodeURIComponent(JSON.stringify([result.outputUrl]))}` url: `/pages/result/index?images=${encodeURIComponent(JSON.stringify([result.outputUrl]))}`,
}) });
return; return;
} else if (result.status === 'failed') { } else if (result.status === 'failed') {
setStatus('error') setStatus('error');
setResult({ success: false, error: result.error || '处理失败' }) setResult({ success: false, error: result.error || '处理失败' });
} else if (attempts < maxAttempts) { } else if (attempts < maxAttempts) {
// 继续轮询 timerRef = setTimeout(poll, 1000);
timerRef = setTimeout(poll, 1000)
} else { } else {
// 超时 setStatus('error');
setStatus('error') setResult({ success: false, error: '处理超时' });
setResult({ success: false, error: '处理超时' })
} }
} catch (error) { } catch (error) {
if (attempts < maxAttempts) { if (attempts < maxAttempts) {
timerRef = setTimeout(poll, 1000) timerRef = setTimeout(poll, 1000);
} else { } else {
setStatus('error') setStatus('error');
setResult({ success: false, error: '获取结果失败' }) setResult({ success: false, error: '获取结果失败' });
} }
} }
} };
poll() poll();
} catch (error) { } catch (error) {
setStatus('error') setStatus('error');
setResult({ success: false, error: '开始处理失败' }) setResult({ success: false, error: '开始处理失败' });
} }
} };
const handleTryAgain = () => { const handleTryAgain = () => {
// 再来一张,返回首页 switchTab({ url: '/pages/home/index' });
switchTab({ url: '/pages/home/index' }) };
}
const handleViewHistory = () => { const handleViewHistory = () => {
// 查看记录,切换到历史记录页 switchTab({ url: '/pages/history/index' });
switchTab({ url: '/pages/history/index' }) };
}
const renderLoading = () => ( const renderLoading = () => (
<View className='generate-loading'> <View className="generate-loading">
<LoadingOverlay> <LoadingOverlay>
<View className='action-buttons'> <View className="action-buttons">
<Button className='btn-primary' onClick={handleTryAgain}> <Button className="btn-primary" onClick={handleTryAgain}>
</Button> </Button>
<Button className='btn-secondary' onClick={handleViewHistory}> <Button className="btn-secondary" onClick={handleViewHistory}>
</Button> </Button>
</View> </View>
</LoadingOverlay> </LoadingOverlay>
</View> </View>
) );
const renderError = () => ( const renderError = () => (
<View className='generate-error'> <View className="generate-error">
<View className='error-container'> <View className="error-container">
<Text className='error-text'></Text> <Text className="error-text"></Text>
<Text className='error-message'>{result?.error || '未知错误'}</Text> <Text className="error-message">{result?.error || '未知错误'}</Text>
</View> </View>
<View className='action-buttons'> <View className="action-buttons">
<Button className='btn-primary' onClick={handleTryAgain}> <Button className="btn-primary" onClick={handleTryAgain}>
</Button> </Button>
<Button className='btn-secondary' onClick={handleViewHistory}> <Button className="btn-secondary" onClick={handleViewHistory}>
</Button> </Button>
</View> </View>
</View> </View>
) );
return ( return (
<View className='generate'> <View className="generate">
{status === 'loading' && renderLoading()} {status === 'loading' && renderLoading()}
{status === 'error' && renderError()} {status === 'error' && renderError()}
</View> </View>
) );
} }

View File

@@ -1,7 +1,6 @@
.result-page { .result-page {
width: 100%; width: 100%;
height: 100vh; height: 100vh;
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@@ -10,31 +9,29 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 40rpx; padding: 20px;
background: transparent;
color: white; color: white;
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
z-index: 10; z-index: 10;
border-bottom: 2rpx solid rgba(255, 255, 255, 0.1); border-bottom: 1px solid rgb(255 255 255 / 10%);
} }
.result-title { .result-title {
font-size: 36rpx; font-size: 18px;
font-weight: bold; font-weight: bold;
} }
.close-btn { .close-btn {
width: 80rpx; width: 40px;
height: 80rpx; height: 40px;
background: rgba(255, 255, 255, 0.2);
color: white; color: white;
border: none; border: none;
border-radius: 40rpx; border-radius: 20px;
font-size: 48rpx; font-size: 24px;
line-height: 80rpx; line-height: 40px;
text-align: center; text-align: center;
} }
@@ -50,8 +47,6 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
background: rgba(0, 0, 0, 0.3);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@@ -62,16 +57,7 @@
.backdrop-blur::before { .backdrop-blur::before {
content: ''; content: '';
position: absolute; position: absolute;
top: 0; inset: 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%
);
pointer-events: none; pointer-events: none;
z-index: 1; z-index: 1;
} }
@@ -83,20 +69,20 @@
height: 100%; height: 100%;
z-index: 2; z-index: 2;
position: relative; position: relative;
border-radius: 0rpx; border-radius: 0;
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.3); box-shadow: 0 10px 30px rgb(0 0 0 / 30%);
} }
/* 模板处理结果对比样式 */ /* 模板处理结果对比样式 */
.template-name { .template-name {
font-size: 36rpx; font-size: 18px;
font-weight: 700; font-weight: 700;
margin-bottom: 8rpx; margin-bottom: 4px;
text-align: center; text-align: center;
} }
.result-status { .result-status {
font-size: 28rpx; font-size: 14px;
opacity: 0.8; opacity: 0.8;
text-align: center; text-align: center;
} }
@@ -107,8 +93,8 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 40rpx 32rpx; padding: 20px 16px;
gap: 32rpx; gap: 16px;
} }
.comparison-item { .comparison-item {
@@ -121,23 +107,22 @@
.comparison-image { .comparison-image {
width: 100%; width: 100%;
max-height: 600rpx; max-height: 300px;
border-radius: 24rpx; border-radius: 12px;
box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.2); box-shadow: 0 4px 20px rgb(0 0 0 / 20%);
background: #fff;
} }
.comparison-label { .comparison-label {
margin-top: 16rpx; margin-top: 8px;
color: #fff; color: #fff;
font-size: 28rpx; font-size: 14px;
font-weight: 600; font-weight: 600;
text-align: center; text-align: center;
} }
.comparison-arrow { .comparison-arrow {
color: #fff; color: #fff;
font-size: 48rpx; font-size: 24px;
font-weight: bold; font-weight: bold;
opacity: 0.8; opacity: 0.8;
} }
@@ -151,12 +136,11 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
background: black;
} }
.result-video { .result-video {
width: 100%; width: 100%;
height: 100%; height: 100%;
max-height: 80vh; max-height: 80vh;
border-radius: 0rpx; border-radius: 0;
} }

View File

@@ -1,239 +1,149 @@
import { View, Image, Video } from '@tarojs/components' import { Image, Video, View } from '@tarojs/components';
import { useEffect, useState } from 'react' import {
import { saveImageToPhotosAlbum, saveVideoToPhotosAlbum, navigateTo, downloadFile, showToast, navigateBack, getCurrentInstance, hideToast } from '@tarojs/taro' downloadFile,
import DownloadSection from '../../components/DownloadSection' getCurrentInstance,
import { useAd } from '../../hooks/useAd' hideToast,
import { useServerSdk } from '../../hooks/index' navigateBack,
import './index.css' 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 ResultPage: React.FC = () => {
const [images, setImages] = useState<string[]>([]) const [images, setImages] = useState<string[]>([]);
const [mediaType, setMediaType] = useState<'image' | 'video'>('image') const [mediaType, setMediaType] = useState<'image' | 'video'>('image');
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false);
const serverSdk = useServerSdk() const serverSdk = useServerSdk();
// 集成广告钩子 const {
const { showAd, loading: adLoading, adAvailable } = useAd({ showAd,
loading: adLoading,
adAvailable,
} = useAd({
onReward: () => { onReward: () => {
// 观看完整广告后的奖励回调 handleDownloadImages();
handleDownloadImages()
}, },
onClose: (isEnded) => { onClose: isEnded => {
if (!isEnded) { if (!isEnded) {
showToast({ showToast({
title: '请观看完整广告才能下载', title: '请观看完整广告才能下载',
icon: 'none', icon: 'none',
duration: 2000 duration: 2000,
}) });
} }
} },
}) });
useEffect(() => { useEffect(() => {
// 获取页面参数 const instance = getCurrentInstance();
const instance = getCurrentInstance() const params = instance?.router?.params;
const params = instance?.router?.params
// 兼容原有的多图片参数
if (params?.images) { if (params?.images) {
try { try {
const imageList = JSON.parse(decodeURIComponent(params.images)) const imageList = JSON.parse(decodeURIComponent(params.images));
setImages(imageList) setImages(imageList);
// 检测媒体类型
if (imageList.length > 0) { if (imageList.length > 0) {
const firstUrl = imageList[0] const firstUrl = imageList[0];
const isVideo = /\.(mp4|webm|ogg|mov|avi|mkv|flv)$/i.test(firstUrl) const isVideo = /\.(mp4|webm|ogg|mov|avi|mkv|flv)$/i.test(firstUrl);
setMediaType(isVideo ? 'video' : 'image') setMediaType(isVideo ? 'video' : 'image');
} }
} catch (error) { } catch (error) {
console.error('解析图片参数失败:', error) console.error('解析图片参数失败:', error);
showToast({ showToast({
title: '参数错误', title: '参数错误',
icon: 'error' icon: 'error',
}) });
navigateBack() navigateBack();
} }
} else { } else {
showToast({ showToast({
title: '缺少图片参数', title: '缺少图片参数',
icon: 'error' icon: 'error',
}) });
navigateBack() 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 handleDownloadImages = async () => {
// 优先处理模板结果媒体文件 const mediaUrl = images.length > 0 ? images[0] : '';
const mediaUrl = images.length > 0 ? images[0] : '' if (!mediaUrl) return;
if (!mediaUrl) return
console.log({ mediaUrl }) console.log({ mediaUrl });
try { try {
// 从 mediaUrl 中提取文件信息
const getFileInfoFromUrl = (url: string) => { const getFileInfoFromUrl = (url: string) => {
// 移除查询参数和锚点 const cleanUrl = url.split('?')[0].split('#')[0];
const cleanUrl = url.split('?')[0].split('#')[0] const fileName = cleanUrl.substring(cleanUrl.lastIndexOf('/') + 1) || 'download';
// 提取文件名 const extensionMatch = fileName.match(/\.([^.]+)$/);
const fileName = cleanUrl.substring(cleanUrl.lastIndexOf('/') + 1) || 'download' const extension = extensionMatch ? extensionMatch[1].toLowerCase() : '';
// 提取扩展名
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) { 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({ const downloadRes = await downloadFile({
url: mediaUrl, url: mediaUrl,
header: { header: {
'Accept': 'image/*, video/*', Accept: 'image/*, video/*',
'User-Agent': 'Mozilla/5.0 (compatible; MiniApp)' 'User-Agent': 'Mozilla/5.0 (compatible; MiniApp)',
} },
}) });
hideToast() hideToast();
// 使用实际的文件路径 const filePath = downloadRes.tempFilePath;
const filePath = downloadRes.tempFilePath
showToast({ title: '保存中...', icon: 'loading' }) showToast({ title: '保存中...', icon: 'loading' });
// 根据实际文件类型选择保存方法
if (mediaType === 'video') { if (mediaType === 'video') {
console.log('保存为视频文件') console.log('保存为视频文件');
await saveVideoToPhotosAlbum({ filePath }) await saveVideoToPhotosAlbum({ filePath });
} else if (mediaType === 'image') { } else if (mediaType === 'image') {
console.log('保存为图片文件') console.log('保存为图片文件');
await saveImageToPhotosAlbum({ filePath }) await saveImageToPhotosAlbum({ filePath });
} else { } else {
// 如果无法确定文件类型,默认尝试保存为图片 console.log('文件类型不确定,尝试保存为图片');
console.log('文件类型不确定,尝试保存为图片')
try { try {
await saveImageToPhotosAlbum({ filePath }) await saveImageToPhotosAlbum({ filePath });
} catch (imageError) { } catch (imageError) {
console.log('图片保存失败,尝试视频保存') console.log('图片保存失败,尝试视频保存');
await saveVideoToPhotosAlbum({ filePath }) await saveVideoToPhotosAlbum({ filePath });
} }
} }
showToast({ showToast({
title: '保存成功', title: '保存成功',
icon: 'success', icon: 'success',
duration: 2000 duration: 2000,
}) });
} catch (error) { } catch (error) {
console.error('保存失败:', error) console.error('保存失败:', error);
// 分析错误类型,提供更具体的错误提示 let errorMsg = '保存失败,请重试';
let errorMsg = '保存失败,请重试'
if (error && typeof error === 'object') { if (error && typeof error === 'object') {
const err = error as any const err = error as any;
if (err.errMsg) { if (err.errMsg) {
console.error('详细错误信息:', err.errMsg) console.error('详细错误信息:', err.errMsg);
if (err.errMsg.includes('auth')) { if (err.errMsg.includes('auth')) {
errorMsg = '权限不足,请在设置中开启相册权限' errorMsg = '权限不足,请在设置中开启相册权限';
} else if (err.errMsg.includes('network')) { } else if (err.errMsg.includes('network')) {
errorMsg = '网络错误,请检查网络连接' errorMsg = '网络错误,请检查网络连接';
} else if (err.errMsg.includes('invalid file type')) { } else if (err.errMsg.includes('invalid file type')) {
errorMsg = '文件格式不支持,请联系客服' errorMsg = '文件格式不支持,请联系客服';
} else if (err.errMsg.includes('file')) { } else if (err.errMsg.includes('file')) {
errorMsg = '文件格式不支持或已损坏' errorMsg = '文件格式不支持或已损坏';
} else if (err.errMsg.includes('download')) { } else if (err.errMsg.includes('download')) {
errorMsg = '文件下载失败,请重试' errorMsg = '文件下载失败,请重试';
} }
} }
} }
@@ -241,92 +151,79 @@ const ResultPage: React.FC = () => {
showToast({ showToast({
title: errorMsg, title: errorMsg,
icon: 'error', icon: 'error',
duration: 3000 duration: 3000,
}) });
} finally { } finally {
} }
} };
// 再来一张
const handleRegenerate = () => { const handleRegenerate = () => {
navigateBack() navigateBack();
} };
// 图片生视频
const handleImageToVideo = async () => { const handleImageToVideo = async () => {
if (mediaType !== 'image' || !images[0]) { if (mediaType !== 'image' || !images[0]) {
showToast({ showToast({
title: '仅支持图片生成视频', title: '仅支持图片生成视频',
icon: 'error', icon: 'error',
duration: 2000 duration: 2000,
}) });
return return;
} }
try { try {
setLoading(true) setLoading(true);
showToast({ showToast({
title: '正在生成视频...', title: '正在生成视频...',
icon: 'loading', icon: 'loading',
duration: 60000 duration: 60000,
}) });
const taskId = await serverSdk.executeTemplate({ const taskId = await serverSdk.executeTemplate({
imageUrl: images[0], imageUrl: images[0],
templateCode: 'character_figurine_v1' templateCode: 'character_figurine_v1',
}) });
hideToast() hideToast();
// 跳转到生成页面
navigateTo({ navigateTo({
url: `/pages/generate/index?taskId=${taskId}&templateCode=character_figurine_v1` url: `/pages/generate/index?taskId=${taskId}&templateCode=character_figurine_v1`,
}) });
} catch (error) { } catch (error) {
hideToast() hideToast();
console.error('图片生视频失败:', error) console.error('图片生视频失败:', error);
showToast({ showToast({
title: '生成失败,请重试', title: '生成失败,请重试',
icon: 'error', icon: 'error',
duration: 2000 duration: 2000,
}) });
} finally { } finally {
setLoading(false) setLoading(false);
} }
} };
// 兼容原有的多图片显示
if (!images.length) { if (!images.length) {
return null return null;
} }
return ( return (
<View className='result-page'> <View className="result-page">
{mediaType === 'video' ? ( {mediaType === 'video' ? (
<View className='result-video-container'> <View className="result-video-container">
<Video <Video className="result-video" src={images[0]} autoplay controls={false} muted loop objectFit="contain" />
className='result-video'
src={images[0]}
autoplay
muted
loop
controls
objectFit='contain'
/>
</View> </View>
) : ( ) : (
<View <View
className='result-image-container' className="result-image-container"
style={{ style={{
backgroundImage: `url(${images[0]})`, backgroundImage: `url(${images[0]})`,
backgroundSize: 'cover', backgroundSize: 'cover',
backgroundPosition: 'center', backgroundPosition: 'center',
backgroundRepeat: 'no-repeat' backgroundRepeat: 'no-repeat',
}} }}
> >
<View className='backdrop-blur'> <View className="backdrop-blur">
<Image className='result-image' src={images[0]} mode='aspectFit' /> <Image className="result-image" src={images[0]} mode="aspectFit" />
</View> </View>
</View> </View>
)} )}
@@ -340,7 +237,7 @@ const ResultPage: React.FC = () => {
imageToVideoLoading={loading} imageToVideoLoading={loading}
/> />
</View> </View>
) );
} };
export default ResultPage export default ResultPage;