diff --git a/src/app.config.ts b/src/app.config.ts index 3d07256..588cbe9 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -3,6 +3,7 @@ export default defineAppConfig({ 'pages/home/index', // 新的模板卡片首页 'pages/history/index', // 历史记录页面 'pages/result/index', + 'pages/friends-photo/index', // 好友合照页面 ], tabBar: { color: '#8E9BAE', diff --git a/src/pages/friends-photo/index.config.ts b/src/pages/friends-photo/index.config.ts new file mode 100644 index 0000000..c2b46d0 --- /dev/null +++ b/src/pages/friends-photo/index.config.ts @@ -0,0 +1,5 @@ +export default definePageConfig({ + navigationBarTitleText: '好友合照', + enableShareAppMessage: true, + enableShareTimeline: true, +}); diff --git a/src/pages/friends-photo/index.css b/src/pages/friends-photo/index.css new file mode 100644 index 0000000..a3e2fd5 --- /dev/null +++ b/src/pages/friends-photo/index.css @@ -0,0 +1,427 @@ +/* 页面容器 */ +.friends-photo { + height: 100vh; + background: #f5f6f8; + display: flex; + flex-direction: column; + position: relative; +} + +/* 滚动容器 */ +.friends-photo-scroll { + flex: 1; + height: 100%; + padding-bottom: 100px; /* 为固定底部按钮留空间 */ +} + +/* 介绍卡片 */ +.intro-card { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + margin: 20px; + border-radius: 20px; + padding: 24px; + box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3); + position: relative; + overflow: hidden; +} + +.intro-card::before { + content: ''; + position: absolute; + top: -50%; + right: -50%; + width: 100%; + height: 100%; + background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%); + border-radius: 50%; +} + +.intro-content { + display: flex; + align-items: center; + gap: 16px; + position: relative; + z-index: 1; +} + +.intro-icon { + font-size: 36px; + background: rgba(255, 255, 255, 0.2); + border-radius: 16px; + padding: 12px; + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.3); +} + +.intro-text { + flex: 1; +} + +.intro-title { + font-size: 20px; + font-weight: 600; + color: white; + margin-bottom: 4px; +} + +.intro-subtitle { + font-size: 14px; + color: rgba(255, 255, 255, 0.9); + line-height: 1.4; +} + +/* 上传区域 */ +.upload-section { + padding: 0 20px; + display: flex; + flex-direction: column; + gap: 16px; +} + +/* 上传卡片 */ +.upload-card { + background: white; + border-radius: 16px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); + overflow: hidden; + transition: all 0.3s ease; +} + +.upload-card:active { + transform: scale(0.98); + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12); +} + +.upload-card-content { + padding: 20px; +} + +/* 上传信息 */ +.upload-info { + margin-bottom: 16px; +} + +.upload-title { + font-size: 16px; + font-weight: 600; + color: #1d1f22; + margin-bottom: 4px; +} + +.upload-description { + font-size: 14px; + color: #8e9bae; +} + +/* 上传区域 */ +.upload-area { + width: 100%; + height: 160px; + border: 2px dashed #e5e7eb; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + background: #fafbfc; + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.upload-area:active { + transform: scale(0.98); +} + +.upload-area.has-image { + border: 2px solid #3b82f6; + background: transparent; +} + +/* 上传占位符 */ +.upload-placeholder { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; +} + +.upload-icon { + font-size: 32px; + color: #8e9bae; +} + +.upload-hint { + font-size: 14px; + color: #8e9bae; + font-weight: 500; +} + +/* 上传的图片 */ +.upload-image { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 10px; +} + +/* 图片覆盖层 */ +.image-overlay { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 0.3s ease; + backdrop-filter: blur(4px); +} + +.upload-area.has-image:active .image-overlay { + opacity: 1; +} + +.overlay-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; +} + +.change-icon { + font-size: 24px; + color: white; +} + +.change-text { + color: white; + font-size: 14px; + font-weight: 500; +} + +/* 预览卡片 */ +.preview-card { + background: white; + margin: 24px 20px 0; + border-radius: 16px; + padding: 20px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); + animation: slideUp 0.3s ease-out; +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.preview-header { + margin-bottom: 16px; +} + +.preview-title { + font-size: 16px; + font-weight: 600; + color: #1d1f22; + margin-bottom: 4px; +} + +.preview-subtitle { + font-size: 14px; + color: #8e9bae; +} + +/* 预览网格 */ +.preview-grid { + display: flex; + align-items: center; + gap: 16px; +} + +.preview-item { + flex: 1; + position: relative; +} + +.preview-image { + width: 100%; + aspect-ratio: 1; + object-fit: cover; + border-radius: 12px; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); +} + +.preview-label { + position: absolute; + bottom: 8px; + left: 8px; + background: rgba(0, 0, 0, 0.7); + color: white; + font-size: 12px; + font-weight: 500; + padding: 4px 8px; + border-radius: 6px; + backdrop-filter: blur(4px); +} + +.preview-connector { + font-size: 20px; + font-weight: bold; + color: #3b82f6; + background: rgba(59, 130, 246, 0.1); + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +/* 底部间距 */ +.bottom-spacer { + height: 20px; +} + +/* 提交区域 */ +.submit-section { + position: fixed; + bottom: 0; + left: 0; + right: 0; + padding: 20px; + background: white; + border-top: 1px solid #e5e7eb; + box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.08); + backdrop-filter: blur(10px); + z-index: 10; +} + +/* 提交按钮 */ +.submit-button { + width: 100%; + height: 56px; + background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); + border-radius: 28px; + border: none; + color: white; + font-size: 16px; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + transition: all 0.3s ease; + box-shadow: 0 6px 24px rgba(59, 130, 246, 0.4); + position: relative; + overflow: hidden; +} + +.submit-button::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.2), transparent); + transform: rotate(45deg); + transition: transform 0.6s ease; +} + +.submit-button:not(.disabled):active::before { + transform: rotate(45deg) translateX(100%); +} + +.submit-button.disabled { + background: #d1d5db; + box-shadow: none; + cursor: not-allowed; +} + +.submit-button:not(.disabled):active { + transform: translateY(2px); + box-shadow: 0 3px 16px rgba(59, 130, 246, 0.4); +} + +.submit-icon { + font-size: 18px; +} + +.submit-text { + font-weight: 600; +} + +/* 加载动画 */ +.loading-spinner { + width: 16px; + height: 16px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-top: 2px solid white; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +/* 响应式适配 */ +@media (max-width: 375px) { + .intro-card { + margin: 16px; + padding: 20px; + } + + .upload-section { + padding: 0 16px; + } + + .preview-card { + margin: 20px 16px 0; + } + + .submit-section { + padding: 16px; + } +} + +/* 深色模式适配 */ +@media (prefers-color-scheme: dark) { + .friends-photo { + background: #1a1a1a; + } + + .upload-card, + .preview-card { + background: #2a2a2a; + border: 1px solid #333; + } + + .upload-title { + color: #fff; + } + + .upload-description { + color: #999; + } + + .upload-area { + background: #333; + border-color: #444; + } + + .submit-section { + background: #2a2a2a; + border-color: #333; + } +} diff --git a/src/pages/friends-photo/index.tsx b/src/pages/friends-photo/index.tsx new file mode 100644 index 0000000..cef9172 --- /dev/null +++ b/src/pages/friends-photo/index.tsx @@ -0,0 +1,322 @@ +import { View, Image, Button, ScrollView } from '@tarojs/components'; +import { useState, useEffect } from 'react'; +import Taro, { navigateTo } from '@tarojs/taro'; +import { useSdk, useServerSdk } from '../../hooks/index'; +import { useImageDetectionTaskManager, ImageAuditResult, AuditConclusion } from '../../hooks/useImageDetectionTaskManager'; + +import './index.css'; + +// 好友合照模板的固定代码 +const FRIENDS_PHOTO_TEMPLATE_CODE = 'friends_photo'; + +export default function FriendsPhoto() { + const sdk = useSdk(); + const serverSdk = useServerSdk(); + const { submitAuditTask } = useImageDetectionTaskManager(); + + const [image1, setImage1] = useState(''); + const [image2, setImage2] = useState(''); + const [loading, setLoading] = useState(false); + const [showGuide, setShowGuide] = useState(true); + + // 显示使用指南 + useEffect(() => { + if (!showGuide) return; + + const timer = setTimeout(() => { + Taro.showModal({ + title: '使用小贴士', + content: '• 选择清晰的人脸照片效果更佳\n• 建议选择光线充足的照片\n• 支持JPG、PNG格式', + confirmText: '我知道了', + showCancel: false, + success: () => { + setShowGuide(false); + } + }); + }, 1000); + + return () => clearTimeout(timer); + }, [showGuide]); + + // 处理审核失败 + const handleAuditFailure = (auditResult: ImageAuditResult) => { + const messages: Record = { + [AuditConclusion.REJECT]: '图片内容不符合规范,请重新选择符合规范的图片', + [AuditConclusion.REVIEW]: '图片需要人工审核,请稍后重试或更换其他图片', + [AuditConclusion.UNCERTAIN]: '图片审核结果不确定,请重新选择图片', + [AuditConclusion.PASS]: '图片审核通过', + }; + + const message = auditResult.conclusion ? messages[auditResult.conclusion] : '图片审核失败,请重新选择图片'; + + Taro.showModal({ + title: '图片审核未通过', + content: message, + confirmText: '我知道了', + showCancel: false, + }); + }; + + // 上传单张图片 + const uploadSingleImage = async (imageIndex: 1 | 2) => { + try { + // 添加触觉反馈 + Taro.vibrateShort(); + + Taro.showLoading({ + title: '选择图片中...', + mask: true, + }); + + const imageUrl = await sdk.chooseAndUploadImage({ + count: 1, + onImageSelected: () => { + Taro.showLoading({ + title: '正在上传...', + mask: true, + }); + }, + onProgress: () => { + Taro.showLoading({ + title: '上传中...', + mask: true, + }); + }, + }); + + // 审核图片 + const auditResult = await new Promise((resolve, reject) => { + submitAuditTask( + { + imageUrl, + options: { + enableCache: true, + timeout: 30000, + }, + }, + (status, progress) => { + console.log('审核进度:', status, progress); + }, + result => { + console.log('审核成功:', result); + resolve(result); + }, + error => { + console.error('审核失败:', error); + reject(error); + } + ); + }); + + Taro.hideLoading(); + + if (auditResult.conclusion === AuditConclusion.PASS) { + if (imageIndex === 1) { + setImage1(imageUrl); + } else { + setImage2(imageUrl); + } + + // 成功反馈 + Taro.vibrateShort(); + Taro.showToast({ + title: `图片${imageIndex}上传成功`, + icon: 'success', + duration: 1500, + }); + } else { + handleAuditFailure(auditResult); + } + } catch (error: any) { + Taro.hideLoading(); + + // 用户取消选择图片,不显示错误提示 + if (error.errMsg && error.errMsg.includes('chooseImage:fail cancel')) { + return; + } + + // 提供更详细的错误提示 + let errorTitle = '图片上传失败'; + if (error?.message) { + errorTitle = error.message; + } else if (error?.errMsg) { + if (error.errMsg.includes('chooseImage:fail auth')) { + errorTitle = '需要授权访问相册'; + } else if (error.errMsg.includes('timeout')) { + errorTitle = '上传超时,请重试'; + } else if (error.errMsg.includes('network')) { + errorTitle = '网络连接失败'; + } else { + errorTitle = '图片上传失败,请重试'; + } + } + + Taro.showToast({ + title: errorTitle, + icon: 'error', + duration: 3000, + }); + + console.error('图片上传详细错误:', error); + } + }; + + // 提交处理 + const handleSubmit = async () => { + if (!image1 || !image2) { + Taro.vibrateShort(); + Taro.showToast({ + title: '请先上传两张图片', + icon: 'none', + duration: 2000, + }); + return; + } + + // 提交时的触觉反馈 + Taro.vibrateShort(); + setLoading(true); + + try { + Taro.showLoading({ + title: 'AI正在生成中...', + mask: true, + }); + + // 将两个图片URL用逗号分隔 + const combinedImageUrl = `${image1},${image2}`; + + const taskId = await serverSdk.executeTemplate({ + imageUrl: combinedImageUrl, + templateCode: FRIENDS_PHOTO_TEMPLATE_CODE, + }); + + Taro.hideLoading(); + + // 跳转到结果页面 + navigateTo({ + url: `/pages/result/index?taskId=${taskId}&templateCode=${FRIENDS_PHOTO_TEMPLATE_CODE}`, + }); + } catch (error) { + Taro.hideLoading(); + console.error('提交失败:', error); + + Taro.showToast({ + title: '请等待生成中的任务完成后再尝试', + icon: 'none', + duration: 2000, + }); + } finally { + setLoading(false); + } + }; + + const renderUploadCard = (imageIndex: 1 | 2, imageUrl: string, title: string, description: string) => ( + + + + {title} + {description} + + uploadSingleImage(imageIndex)} + > + {imageUrl ? ( + <> + + + + 🔄 + 点击更换 + + + + ) : ( + + 📷 + 点击上传 + + )} + + + + ); + + return ( + + + {/* 顶部介绍卡片 */} + + + 👫 + + 好友合照生成 + + 上传两张照片,AI将为你们生成精彩的合照视频 + + + + + + {/* 上传区域 */} + + {renderUploadCard(1, image1, "第一张照片", "选择你的照片")} + {renderUploadCard(2, image2, "第二张照片", "选择朋友的照片")} + + + {/* 预览区域 */} + {image1 && image2 && ( + + + 📸 照片预览 + 确认无误后即可开始生成 + + + + + 照片 1 + + + + + + 照片 2 + + + + )} + + {/* 底部间距 */} + + + + {/* 固定底部按钮 */} + + + + + ); +} diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx index 906aeeb..c3f9725 100644 --- a/src/pages/home/index.tsx +++ b/src/pages/home/index.tsx @@ -1,4 +1,4 @@ -import { View, ScrollView, Canvas } from '@tarojs/components'; +import { View, ScrollView } from '@tarojs/components'; import { useEffect, useState } from 'react'; import Taro, { navigateTo } from '@tarojs/taro'; import { useAppDispatch, useAppSelector } from '../../hooks/redux'; @@ -90,9 +90,15 @@ export default function Home() { }; const handleTemplateClick = async (template: Template) => { + // 如果是好友合照,跳转到专门的页面 + if (template.name === '好友合照') { + navigateTo({ url: `/pages/friends-photo/index` }); + return; + } + try { // 第一步:选择并上传图片 - const imageCount = template.templateType === '2image-to-video' ? 2 : 1; + const imageCount = 1; const imageUrl = await sdk.chooseAndUploadImage({ count: imageCount, onImageSelected: () => { diff --git a/src/sdk/bowongAISDK.ts b/src/sdk/bowongAISDK.ts index e91402d..5e53415 100644 --- a/src/sdk/bowongAISDK.ts +++ b/src/sdk/bowongAISDK.ts @@ -1,3 +1,4 @@ +import { PlatformFactory } from "../platforms/factory"; import Taro from "@tarojs/taro" /** @@ -51,7 +52,7 @@ interface ImageToVideoParams { } export function isGetFileInfoSuccessCallbackResult(val: any): val is Taro.getFileInfo.SuccessCallbackResult { - return val && Reflect.has(val, 'digest') + return val && typeof val.size === 'number' && !val.errMsg } /** @@ -74,11 +75,25 @@ export class BowongAISDK { const url = `${this.baseUrl}/api/file/upload/s3`; try { - // 获取文件信息 - const fileInfo = await Taro.getFileInfo({ filePath }); - if (!isGetFileInfoSuccessCallbackResult(fileInfo)) { - throw new Error(fileInfo.errMsg) + // 检测环境并使用不同的上传策略 + if (PlatformFactory.detectPlatform() === 'h5') { + return await this._uploadForH5(params); } + + // 小程序环境的原有逻辑 + // 获取文件信息 + let fileInfo; + try { + fileInfo = await Taro.getFileInfo({ filePath }); + if (!isGetFileInfoSuccessCallbackResult(fileInfo)) { + console.warn('获取文件信息失败,使用默认值:', fileInfo.errMsg); + fileInfo = { size: 0 }; + } + } catch (getFileInfoError) { + console.warn('getFileInfo调用失败,使用默认值:', getFileInfoError); + fileInfo = { size: 0 }; + } + // 自动生成文件名(如果未提供) const fileName = name || `upload_${Date.now()}.${this._getFileExtension(filePath)}`; @@ -138,8 +153,105 @@ export class BowongAISDK { console.log('文件上传成功:', result.data); return result.data; - } catch (error) { + } catch (error: any) { console.error('文件上传失败:', error); + + // 提供更详细的错误信息 + let errorMessage = '文件上传失败'; + if (error?.errMsg) { + if (error.errMsg.includes('timeout')) { + errorMessage = '上传超时,请检查网络连接'; + } else if (error.errMsg.includes('network')) { + errorMessage = '网络连接失败,请检查网络'; + } else if (error.errMsg.includes('size')) { + errorMessage = '文件过大,请选择较小的图片'; + } else if (error.errMsg.includes('cors') || error.errMsg.includes('CORS')) { + errorMessage = 'H5环境跨域错误,请联系开发者'; + } else { + errorMessage = `上传失败: ${error.errMsg}`; + } + } else if (error?.message) { + if (error.message.includes('cors') || error.message.includes('CORS')) { + errorMessage = 'H5环境跨域错误,请联系开发者'; + } else { + errorMessage = error.message; + } + } + + throw new Error(errorMessage); + } + } + + /** + * H5环境专用上传方法 + * @private + */ + private async _uploadForH5(params: UploadParams): Promise { + const { filePath, name, type } = params; + const url = `${this.baseUrl}/api/file/upload/s3`; + + try { + // H5环境中,filePath实际上是File对象或Blob URL + let file: File; + + if (typeof filePath === 'string') { + // 如果是blob URL,需要先获取blob + const response = await fetch(filePath); + const blob = await response.blob(); + const fileName = name || `upload_${Date.now()}.${this._getFileExtension(filePath)}`; + file = new File([blob], fileName, { type: type || blob.type }); + } else if (filePath && typeof filePath === 'object' && 'name' in filePath && 'size' in filePath && 'type' in filePath) { + file = filePath as File; + } else { + throw new Error('H5环境下文件路径格式不正确'); + } + + // 创建FormData + const formData = new FormData(); + formData.append('file', file); + formData.append('filename', file.name); + formData.append('type', file.type); + + console.log('H5环境开始上传文件:', { + fileName: file.name, + fileSize: file.size, + fileType: file.type + }); + + // 使用fetch API上传,添加CORS支持 + const response = await fetch(url, { + method: 'POST', + body: formData, + headers: { + 'Accept': 'application/json', + // 不要手动设置Content-Type,让浏览器自动设置multipart/form-data边界 + }, + mode: 'cors', // 明确指定CORS模式 + credentials: 'omit' // 不发送cookie + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: 文件上传失败`); + } + + const result = await response.json() as ApiResponse; + + // 检查业务状态码 + if (!result.status) { + throw new Error(result.msg || '文件上传失败'); + } + + console.log('H5环境文件上传成功:', result.data); + return result.data; + + } catch (error: any) { + console.error('H5环境文件上传失败:', error); + + // 特殊处理CORS错误 + if (error.name === 'TypeError' && error.message.includes('fetch')) { + throw new Error('网络错误:可能是CORS问题,请确保服务器支持跨域请求'); + } + throw error; } } @@ -203,14 +315,14 @@ export class BowongAISDK { } catch (error) { console.error('选择并上传图片失败:', error); - + // 检查是否是授权被拒绝的错误 if (this._isAuthorizationError(error)) { await this._handleAuthorizationError(); // 用户重新授权后,递归调用自己重试 return this.chooseAndUploadImage(options); } - + throw error; } } @@ -223,10 +335,10 @@ export class BowongAISDK { if (!error || typeof error.errMsg !== 'string') { return false; } - + const errMsg = error.errMsg.toLowerCase(); - return errMsg.includes('auth') || - errMsg.includes('permission') || + return errMsg.includes('auth') || + errMsg.includes('permission') || errMsg.includes('denied') || errMsg.includes('authorize') || errMsg.includes('scope'); @@ -251,8 +363,8 @@ export class BowongAISDK { success: (settingResult) => { console.log('用户设置结果:', settingResult); // 检查是否授权了相册权限 - if (settingResult.authSetting && - (settingResult.authSetting['scope.writePhotosAlbum'] || + if (settingResult.authSetting && + (settingResult.authSetting['scope.writePhotosAlbum'] || settingResult.authSetting['scope.camera'])) { Taro.showToast({ title: '授权成功', @@ -341,14 +453,14 @@ export class BowongAISDK { } catch (error) { console.error('选择并上传图片失败:', error); - + // 检查是否是授权被拒绝的错误 if (this._isAuthorizationError(error)) { await this._handleAuthorizationError(); // 用户重新授权后,递归调用自己重试 return this.chooseAndGenerateImage(options); } - + throw error; } } @@ -385,7 +497,7 @@ export class BowongAISDK { * 生成图像 * @param params 图像生成参数 * @returns Promise 返回任务ID - * + * * 使用 multipart/form-data 格式提交,支持文件上传和URL参数 * 对应 curl 命令: * curl -X 'POST' \ @@ -413,17 +525,22 @@ export class BowongAISDK { const url = `${this.baseUrl}/api/custom/image/submit/task`; try { - // 改为选择图片 - // 如果提供了本地文件路径,使用 uploadFile if (img_file) { + // 检测环境并使用不同的上传策略 + if (PlatformFactory.detectPlatform() === 'h5') { + return await this._generateImageForH5(params); + } + + // 小程序环境的原有逻辑 const response = await new Promise((resolve, reject) => { Taro.uploadFile({ url, filePath: img_file, name: 'img_file', header: { - 'accept': 'application/json' + 'accept': 'application/json', + 'Content-Type': 'multipart/form-data' }, formData: { prompt, @@ -458,6 +575,92 @@ export class BowongAISDK { } } + /** + * H5环境专用图像生成方法 + * @private + */ + private async _generateImageForH5(params: GenerateImageParams): Promise { + const { + prompt, + model_name = 'gemini-2.5-flash-image-preview', + aspect_ratio = '9:16', + mode = 'turbo', + webhook_flag = false, + img_file + } = params; + + const url = `${this.baseUrl}/api/custom/image/submit/task`; + + try { + // H5环境中,img_file实际上是File对象或Blob URL + let file: File; + + if (typeof img_file === 'string') { + // 如果是blob URL,需要先获取blob + const response = await fetch(img_file); + const blob = await response.blob(); + const fileName = `generate_${Date.now()}.${this._getFileExtension(img_file)}`; + file = new File([blob], fileName, { type: blob.type }); + } else if (img_file && typeof img_file === 'object' && 'name' in img_file && 'size' in img_file && 'type' in img_file) { + file = img_file as File; + } else { + throw new Error('H5环境下图片文件格式不正确'); + } + + // 创建FormData + const formData = new FormData(); + formData.append('img_file', file); + formData.append('prompt', prompt); + formData.append('model_name', model_name); + formData.append('aspect_ratio', aspect_ratio); + formData.append('mode', mode.toString()); + formData.append('webhook_flag', webhook_flag.toString()); + formData.append('img_url', ''); + + console.log('H5环境开始图像生成:', { + fileName: file.name, + fileSize: file.size, + fileType: file.type, + prompt: prompt.substring(0, 100) + '...' + }); + + // 使用fetch API上传,添加CORS支持 + const response = await fetch(url, { + method: 'POST', + body: formData, + headers: { + 'accept': 'application/json', + // 不要手动设置Content-Type,让浏览器自动设置multipart/form-data边界 + }, + mode: 'cors', // 明确指定CORS模式 + credentials: 'omit' // 不发送cookie + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: 图像生成请求失败`); + } + + const result = await response.json() as ApiResponse; + + if (!result.status) { + throw new Error(result.msg || '图像生成请求失败'); + } + + console.log('H5环境图像生成任务提交成功:', result.data); + return result.data; + + } catch (error: any) { + console.error('H5环境图像生成失败:', error); + + // 特殊处理CORS错误 + if (error.name === 'TypeError' && error.message.includes('fetch')) { + throw new Error('网络错误:可能是CORS问题,请确保服务器支持跨域请求'); + } + + throw error; + } + } + /** * 查询任务状态 * @param taskId 任务ID @@ -511,7 +714,7 @@ export class BowongAISDK { * 图像转视频 * @param params 图像转视频参数 * @returns Promise 返回任务ID - * + * * 对应 curl 命令: * curl -X 'POST' \ * 'https://bowongai-test--text-video-agent-fastapi-app.modal.run/api/custom/transform/i2v' \ @@ -561,21 +764,21 @@ export class BowongAISDK { /** * 使用示例: - * + * * // 1. 选择并上传图片 * const uploadResult = await bowongAI.chooseAndUploadImage({ * onProgress: (progress) => console.log(`上传进度: ${progress}%`) * }); - * + * * // 2. 使用上传的图片生成新图像 * const generateResult = await bowongAI.generateImage({ * img_url: uploadResult.file_url, * prompt: '转换为油画风格' * }); - * + * * // 3. 查询生成状态 * const status = await bowongAI.getTaskStatus(generateResult.task_id); - * + * * // 4. 获取最终结果 * const finalResult = await bowongAI.getTaskResult(generateResult.task_id); */ @@ -584,4 +787,4 @@ export class BowongAISDK { export const bowongAI = new BowongAISDK(); // 向后兼容的类名导出 -export const Sdk = BowongAISDK; \ No newline at end of file +export const Sdk = BowongAISDK; diff --git a/src/utils/imageUploader.ts b/src/utils/imageUploader.ts deleted file mode 100644 index 1f25304..0000000 --- a/src/utils/imageUploader.ts +++ /dev/null @@ -1,168 +0,0 @@ -import Taro from '@tarojs/taro'; - -/** - * 上传参数接口 - */ -export interface UploadParams { - filePath: string; - name?: string; - type?: string; - onProgress?: (progress: number) => void; -} - -/** - * API响应接口 - */ -interface ApiResponse { - status: boolean; - msg: string; - data: T; -} - -/** - * 获取文件信息成功回调结果类型守卫 - */ -function isGetFileInfoSuccessCallbackResult( - result: any -): result is Taro.getFileInfo.SuccessCallbackResult { - return result && typeof result.size === 'number'; -} - -/** - * 图片上传工具类 - */ -export class ImageUploader { - private baseUrl: string; - - constructor(baseUrl: string = 'https://bowongai-test--text-video-agent-fastapi-app.modal.run') { - this.baseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl; - } - - /** - * 上传文件 - */ - async upload(params: UploadParams): Promise { - const { filePath, name, type, onProgress } = params; - const url = `${this.baseUrl}/api/file/upload/s3`; - - try { - // 获取文件信息 - let fileInfo; - try { - fileInfo = await Taro.getFileInfo({ filePath }); - if (!isGetFileInfoSuccessCallbackResult(fileInfo)) { - console.warn('获取文件信息失败,使用默认值'); - fileInfo = { size: 0 }; - } - } catch (getFileInfoError) { - console.warn('getFileInfo调用失败,使用默认值:', getFileInfoError); - fileInfo = { size: 0 }; - } - - // 自动生成文件名(如果未提供) - const fileName = name || `upload_${Date.now()}.${this._getFileExtension(filePath)}`; - - // 自动判断文件类型(如果未提供) - const fileType = type || this._getMimeType(filePath); - - console.log('开始上传文件:', { - fileName, - fileSize: fileInfo.size, - fileType - }); - - // 使用 Taro.uploadFile 进行文件上传 - const response = await new Promise((resolve, reject) => { - const uploadTask = Taro.uploadFile({ - url, - filePath, - name: 'file', // 服务端接收的字段名 - header: { - 'Accept': 'application/json' - }, - formData: { - filename: fileName, - type: fileType - }, - success: resolve, - fail: reject - }); - - // 监听上传进度 - if (onProgress) { - uploadTask.progress((res) => { - const progress = Math.round((res.totalBytesSent / res.totalBytesExpectedToSend) * 100); - onProgress(progress); - }); - } - }); - - // 检查 HTTP 状态码 - if (response.statusCode !== 200) { - throw new Error(`HTTP ${response.statusCode}: 文件上传失败`); - } - - // 解析响应数据 - let result: ApiResponse; - try { - result = JSON.parse(response.data) as ApiResponse; - } catch (parseError) { - throw new Error('服务器响应格式错误'); - } - - // 检查业务状态码 - if (!result.status) { - throw new Error(result.msg || '文件上传失败'); - } - - console.log('文件上传成功:', result.data); - return result.data; - - } catch (error) { - console.error('文件上传失败:', error); - throw error; - } - } - - /** - * 获取文件扩展名 - */ - private _getFileExtension(filePath: string): string { - const parts = filePath.split('.'); - return parts.length > 1 ? parts[parts.length - 1].toLowerCase() : 'jpg'; - } - - /** - * 获取MIME类型 - */ - private _getMimeType(filePath: string): string { - const ext = this._getFileExtension(filePath); - const mimeTypes: Record = { - 'jpg': 'image/jpeg', - 'jpeg': 'image/jpeg', - 'png': 'image/png', - 'gif': 'image/gif', - 'webp': 'image/webp', - 'bmp': 'image/bmp', - 'svg': 'image/svg+xml' - }; - return mimeTypes[ext] || 'image/jpeg'; - } - - /** - * 设置基础URL - */ - setBaseUrl(baseUrl: string): void { - this.baseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl; - } - - /** - * 获取基础URL - */ - getBaseUrl(): string { - return this.baseUrl; - } -} - -// 导出默认实例 -export const imageUploader = new ImageUploader(); \ No newline at end of file