Merge branch 'zxx_dev' into dev

This commit is contained in:
非凡主儿 2025-10-22 18:18:15 +08:00
commit 4680f2229a
4 changed files with 260 additions and 42 deletions

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { Modal, Button, Spin, QRCode } from 'antd'; import { Modal, Button, Spin, QRCode, message } from 'antd';
import { import {
Youtube, Youtube,
Instagram, Instagram,
@ -11,6 +11,7 @@ import {
Loader2 Loader2
} from 'lucide-react'; } from 'lucide-react';
import { shareApiUrl } from '@/lib/env'; import { shareApiUrl } from '@/lib/env';
import { VideoShareForm } from './VideoShareForm';
interface ShareModalProps { interface ShareModalProps {
/** 是否显示弹框 */ /** 是否显示弹框 */
@ -88,6 +89,42 @@ const sharePlatforms: SharePlatform[] = [
export default function ShareModal({ visible, onClose, project }: ShareModalProps) { export default function ShareModal({ visible, onClose, project }: ShareModalProps) {
const [loadingPlatform, setLoadingPlatform] = useState<string | null>(null); const [loadingPlatform, setLoadingPlatform] = useState<string | null>(null);
const [showShareForm, setShowShareForm] = useState(false);
const [authError, setAuthError] = useState<string | null>(null);
const [selectedPlatform, setSelectedPlatform] = useState<string>('');
/**
* URL
*/
useEffect(() => {
if (!visible) return;
const urlParams = new URLSearchParams(window.location.search);
const platform = urlParams.get('platform');
const status = urlParams.get('status');
const error = urlParams.get('error');
if (platform && status) {
// 统一转换成小写
const normalizedPlatform = platform.toLowerCase();
setSelectedPlatform(normalizedPlatform);
if (status === 'success') {
setAuthError(null);
setShowShareForm(true);
message.success(`${platform} 授权成功!`);
} else if (status === 'error') {
setAuthError(error || '授权失败,请重试');
setShowShareForm(true);
message.error(`${platform} 授权失败`);
}
// 清理 URL 参数
const newUrl = window.location.pathname;
window.history.replaceState({}, '', newUrl);
}
}, [visible]);
/** /**
* ID * ID
*/ */
@ -99,11 +136,11 @@ export default function ShareModal({ visible, onClose, project }: ShareModalProp
/** /**
* *
*/ */
const checkPlatformAuth = async (apiPath: string, platformName: string) => { const checkPlatformAuth = async (apiPath: string, platformName: string, platformId: string) => {
const userId = getUserId(); const userId = getUserId();
if (!userId) { if (!userId) {
console.error('用户ID不存在请先登录'); message.error('用户ID不存在请先登录');
return; return;
} }
@ -111,25 +148,29 @@ export default function ShareModal({ visible, onClose, project }: ShareModalProp
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token') || ''}` 'Authorization': `Bearer ${localStorage.getItem('token') || ''}`,
'ngrok-skip-browser-warning': 'true'
} }
}); });
const data = await response.json(); const data = await response.json();
if (data.successful && data.code === 0) { if (data.successful && data.code === 0) {
const { has_valid_token, auth_url, access_token } = data.data; const { has_valid_token, auth_url } = data.data;
if (!has_valid_token) { if (!has_valid_token) {
// 需要用户授权,跳转到授权页面 // 需要用户授权,跳转到授权页面
window.open(auth_url, '_blank'); window.open(auth_url, '_blank');
message.info('请在新窗口完成授权');
} else { } else {
// 用户已授权直接使用access_token进行第三方登录 // 用户已授权,直接显示上传表单
console.log(`${platformName}用户已授权access_token:`, access_token); setSelectedPlatform(platformId);
// TODO: 实现视频上传逻辑 setAuthError(null);
setShowShareForm(true);
message.success(`${platformName} 已授权,可以直接上传视频`);
} }
} else { } else {
console.error(`获取${platformName}授权信息失败:`, data.message); message.error(data.message || `获取${platformName}授权信息失败`);
} }
}; };
@ -146,14 +187,15 @@ export default function ShareModal({ visible, onClose, project }: ShareModalProp
if (platformId === 'copy') { if (platformId === 'copy') {
await handleCopyLink(); await handleCopyLink();
} else if (platformId === 'instagram') { } else if (platformId === 'instagram') {
alert('Instagram分享功能正在开发中敬请期待'); message.info('Instagram分享功能正在开发中敬请期待');
} else if (platform.apiPath) { } else if (platform.apiPath) {
await checkPlatformAuth(platform.apiPath, platform.name); await checkPlatformAuth(platform.apiPath, platform.name, platformId);
} else { } else {
console.log(`Share to ${platformId} not implemented yet`); console.log(`Share to ${platformId} not implemented yet`);
} }
} catch (error) { } catch (error) {
console.error(`Share to ${platformId} failed:`, error); console.error(`Share to ${platformId} failed:`, error);
message.error(`操作失败:${error instanceof Error ? error.message : '未知错误'}`);
} finally { } finally {
setLoadingPlatform(null); setLoadingPlatform(null);
} }
@ -168,13 +210,13 @@ export default function ShareModal({ visible, onClose, project }: ShareModalProp
const videoUrl = project.final_video_url || project.final_simple_video_url; const videoUrl = project.final_video_url || project.final_simple_video_url;
if (videoUrl) { if (videoUrl) {
await navigator.clipboard.writeText(videoUrl); await navigator.clipboard.writeText(videoUrl);
console.log('视频链接已复制到剪贴板'); message.success('视频链接已复制到剪贴板');
// TODO: 显示复制成功提示
} else { } else {
console.error('视频链接不存在'); message.error('视频链接不存在');
} }
} catch (error) { } catch (error) {
console.error('复制链接失败:', error); console.error('复制链接失败:', error);
message.error('复制链接失败,请手动复制');
} }
}; };
@ -183,18 +225,44 @@ export default function ShareModal({ visible, onClose, project }: ShareModalProp
return project.final_video_url || project.final_simple_video_url || ''; return project.final_video_url || project.final_simple_video_url || '';
}; };
/**
*
*/
const handleVideoShare = (videoUrl: string, videoName: string) => {
// 上传成功后的处理
setShowShareForm(false);
setSelectedPlatform('');
setAuthError(null);
// 延迟关闭主对话框,让用户看到成功提示
setTimeout(() => {
onClose();
}, 1000);
};
/**
*
*/
const handleCancelShareForm = () => {
setShowShareForm(false);
setAuthError(null);
setSelectedPlatform('');
};
return ( return (
<Modal <Modal
title={ title={
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Share2 className="w-5 h-5 text-white" /> <Share2 className="w-5 h-5 text-white" />
<span className="text-white font-medium">Share Video</span> <span className="text-white font-medium">
{showShareForm ? `分享到 ${selectedPlatform}` : 'Share Video'}
</span>
</div> </div>
} }
open={visible} open={visible}
onCancel={onClose} onCancel={onClose}
footer={null} footer={null}
width={720} width={showShareForm ? 500 : 720}
className="share-modal" className="share-modal"
styles={{ styles={{
content: { content: {
@ -212,7 +280,19 @@ export default function ShareModal({ visible, onClose, project }: ShareModalProp
} }
}} }}
> >
<div className="flex gap-6"> {showShareForm ? (
/* 显示视频分享表单 */
<VideoShareForm
onCancel={handleCancelShareForm}
onShare={handleVideoShare}
error={authError}
platform={selectedPlatform}
videoUrl={getVideoUrl()}
videoName={project.name || ''}
/>
) : (
/* 显示平台选择界面 */
<div className="flex gap-6">
{/* 左侧:二维码区域 */} {/* 左侧:二维码区域 */}
<div className="flex-1 flex flex-col items-center justify-center space-y-4"> <div className="flex-1 flex flex-col items-center justify-center space-y-4">
<div className="bg-white/90 backdrop-blur-sm p-4 rounded-lg border border-white/20"> <div className="bg-white/90 backdrop-blur-sm p-4 rounded-lg border border-white/20">
@ -265,7 +345,7 @@ export default function ShareModal({ visible, onClose, project }: ShareModalProp
</div> </div>
</div> </div>
</div> </div>
)}
</Modal> </Modal>
); );
} }

View File

@ -1,37 +1,154 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { AlertCircle, Loader2 } from 'lucide-react';
import { shareApiUrl } from '@/lib/env';
import { message } from 'antd';
export const VideoShareForm: React.FC<{ onCancel: () => void; onShare: (videoUrl: string, videoName: string) => void }> = ({ onCancel, onShare }) => { interface VideoShareFormProps {
const [videoUrl, setVideoUrl] = useState(''); onCancel: () => void;
const [videoName, setVideoName] = useState(''); onShare: (videoUrl: string, videoName: string) => void;
error?: string | null;
platform?: string;
videoUrl?: string;
videoName?: string;
}
const handleShare = () => { // 平台上传接口映射(统一使用小写 key
onShare(videoUrl, videoName); const PLATFORM_UPLOAD_API: Record<string, string> = {
'youtube': '/api/video-share/youtube/upload',
'tiktok': '/api/video-share/tiktok/upload',
'reddit': '/api/video-share/reddit/upload',
'twitter': '/api/video-share/x/upload',
// Instagram 开发中,暂不支持
// 'instagram': '/api/video-share/instagram/upload',
};
export const VideoShareForm: React.FC<VideoShareFormProps> = ({
onCancel,
onShare,
error = null,
platform = '',
videoUrl: initialVideoUrl = '',
videoName: initialVideoName = ''
}) => {
const [videoUrl, setVideoUrl] = useState(initialVideoUrl);
const [videoName, setVideoName] = useState(initialVideoName);
const [uploading, setUploading] = useState(false);
/**
* ID
*/
const getUserId = () => {
const currentUser = JSON.parse(localStorage.getItem('currentUser') || '{}');
return currentUser.id || currentUser.userId;
};
/**
*
*/
const uploadVideoToPlatform = async (platformName: string, contentUrl: string, title: string) => {
const userId = getUserId();
if (!userId) {
message.error('用户ID不存在请先登录');
return false;
}
// 统一转换成小写后查找 API 路径
const normalizedPlatform = platformName.toLowerCase().replace(/\s+/g, '').replace(/[()]/g, '');
const apiPath = PLATFORM_UPLOAD_API[normalizedPlatform];
if (!apiPath) {
message.error(`平台 ${platformName} 暂不支持或正在开发中`);
return false;
}
try {
const response = await fetch(`${shareApiUrl}${apiPath}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token') || ''}`,
'ngrok-skip-browser-warning': 'true'
},
body: JSON.stringify({
user_id: userId,
content_url: contentUrl,
title: title
})
});
const data = await response.json();
if (data.successful && data.code === 0) {
message.success(`视频已成功上传到 ${platformName}`);
return true;
} else {
message.error(data.message || `上传到 ${platformName} 失败`);
return false;
}
} catch (error) {
console.error(`上传到 ${platformName} 失败:`, error);
message.error(`上传失败:${error instanceof Error ? error.message : '网络错误'}`);
return false;
}
};
const handleShare = async () => {
if (!videoUrl || !videoName) {
message.warning('请填写完整的视频信息');
return;
}
setUploading(true);
try {
const success = await uploadVideoToPlatform(platform, videoUrl, videoName);
if (success) {
onShare(videoUrl, videoName);
}
} finally {
setUploading(false);
}
}; };
return ( return (
<div data-alt="video-share-form-container" className="backdrop-blur-lg bg-white/30 dark:bg-slate-900/30 rounded-lg p-6 shadow-lg"> <div data-alt="video-share-form-container" className="backdrop-blur-lg bg-white/10 dark:bg-slate-900/30 rounded-lg p-6 shadow-lg">
<h2 data-alt="form-title" className="text-2xl font-bold mb-4 text-slate-800 dark:text-slate-200"></h2> <h2 data-alt="form-title" className="text-2xl font-bold mb-4 text-white">
{platform && ` - ${platform}`}
</h2>
{/* 错误信息显示 */}
{error && (
<div className="mb-4 p-3 bg-red-500/20 border border-red-500/50 rounded-lg flex items-start gap-2">
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<p className="text-red-200 text-sm font-medium"></p>
<p className="text-red-300 text-xs mt-1">{error}</p>
</div>
</div>
)}
<div data-alt="form-fields" className="space-y-4"> <div data-alt="form-fields" className="space-y-4">
<div data-alt="video-url-field" className="flex flex-col gap-2"> <div data-alt="video-url-field" className="flex flex-col gap-2">
<label htmlFor="videoUrl" className="text-sm font-medium text-slate-700 dark:text-slate-300"></label> <label htmlFor="videoUrl" className="text-sm font-medium text-white/90"></label>
<Input <Input
id="videoUrl" id="videoUrl"
value={videoUrl} value={videoUrl}
onChange={(e) => setVideoUrl(e.target.value)} onChange={(e) => setVideoUrl(e.target.value)}
placeholder="请输入视频链接" placeholder="请输入视频链接"
className="bg-white/50 dark:bg-slate-800/50 border-slate-300 dark:border-slate-700 text-slate-900 dark:text-slate-100" className="bg-white/10 border-white/20 text-white placeholder:text-white/50"
disabled={!!error}
/> />
</div> </div>
<div data-alt="video-name-field" className="flex flex-col gap-2"> <div data-alt="video-name-field" className="flex flex-col gap-2">
<label htmlFor="videoName" className="text-sm font-medium text-slate-700 dark:text-slate-300"></label> <label htmlFor="videoName" className="text-sm font-medium text-white/90"></label>
<Input <Input
id="videoName" id="videoName"
value={videoName} value={videoName}
onChange={(e) => setVideoName(e.target.value)} onChange={(e) => setVideoName(e.target.value)}
placeholder="请输入视频名称" placeholder="请输入视频名称"
className="bg-white/50 dark:bg-slate-800/50 border-slate-300 dark:border-slate-700 text-slate-900 dark:text-slate-100" className="bg-white/10 border-white/20 text-white placeholder:text-white/50"
disabled={!!error}
/> />
</div> </div>
</div> </div>
@ -40,17 +157,38 @@ export const VideoShareForm: React.FC<{ onCancel: () => void; onShare: (videoUrl
variant="outline" variant="outline"
onClick={onCancel} onClick={onCancel}
data-alt="cancel-button" data-alt="cancel-button"
className="border-slate-400 dark:border-slate-600 text-slate-800 dark:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800" className="border-white/30 text-white hover:bg-white/10"
disabled={uploading}
> >
{error ? '关闭' : '取消'}
</Button>
<Button
onClick={handleShare}
data-alt="share-button"
className="bg-blue-500 hover:bg-blue-600 text-white"
>
</Button> </Button>
{!error && (
<Button
onClick={handleShare}
data-alt="share-button"
className="
relative overflow-hidden
bg-gradient-to-r from-[#6AF4F9] to-[#C039F6]
hover:from-[#5ee3e8] hover:to-[#b028e5]
text-white font-medium
shadow-lg shadow-purple-500/30
hover:shadow-xl hover:shadow-purple-500/50
transition-all duration-300
disabled:opacity-50 disabled:cursor-not-allowed
disabled:shadow-none
"
disabled={!videoUrl || !videoName || uploading}
>
{uploading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
'一键转发'
)}
</Button>
)}
</div> </div>
</div> </div>
); );

View File

@ -375,7 +375,7 @@ export default function CreateToVideo2() {
{(project.final_video_url || project.final_simple_video_url) && ( {(project.final_video_url || project.final_simple_video_url) && (
<div className="absolute top-1 right-1 flex items-center gap-1"> <div className="absolute top-1 right-1 flex items-center gap-1">
{/* 转发按钮 */} {/* 转发按钮 */}
{/* <Tooltip placement="top" title="Share"> <Tooltip placement="top" title="Share">
<Button <Button
size="small" size="small"
type="text" type="text"
@ -384,7 +384,7 @@ export default function CreateToVideo2() {
> >
<Send className="w-4 h-4 text-white" /> <Send className="w-4 h-4 text-white" />
</Button> </Button>
</Tooltip> */} </Tooltip>
{/* 下载按钮 */} {/* 下载按钮 */}
<Tooltip placement="top" title="Download"> <Tooltip placement="top" title="Download">
<Button size="small" type="text" disabled={isLoadingDownloadBtn} className="w-[2.5rem] h-[2.5rem] rounded-full items-center justify-center p-0 hidden group-hover:flex transition-all duration-300 hover:bg-white/15" onClick={(e) => handleDownloadClick(e, project)}> <Button size="small" type="text" disabled={isLoadingDownloadBtn} className="w-[2.5rem] h-[2.5rem] rounded-full items-center justify-center p-0 hidden group-hover:flex transition-all duration-300 hover:bg-white/15" onClick={(e) => handleDownloadClick(e, project)}>

View File

@ -53,7 +53,7 @@ export const getEnvConfig = (): EnvConfig => {
javaUrl: process.env.NEXT_PUBLIC_JAVA_URL || 'https://77.app.java.auth.qikongjian.com', javaUrl: process.env.NEXT_PUBLIC_JAVA_URL || 'https://77.app.java.auth.qikongjian.com',
cutUrl: process.env.NEXT_PUBLIC_CUT_URL || 'https://smartcut.api.movieflow.ai', cutUrl: process.env.NEXT_PUBLIC_CUT_URL || 'https://smartcut.api.movieflow.ai',
cutUrlTo: process.env.NEXT_PUBLIC_CUT_URL_TO || 'https://smartcut.api.movieflow.ai', cutUrlTo: process.env.NEXT_PUBLIC_CUT_URL_TO || 'https://smartcut.api.movieflow.ai',
shareApiUrl: process.env.NEXT_PUBLIC_SHARE_API_URL || 'http://39.97.48.225:8000', shareApiUrl: process.env.NEXT_PUBLIC_SHARE_API_URL || 'https://nebuly-tish-metapsychological.ngrok-free.dev',
// Google OAuth 配置 // Google OAuth 配置
googleClientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || '847079918888-o1nne8d3ij80dn20qurivo987pv07225.apps.googleusercontent.com', googleClientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || '847079918888-o1nne8d3ij80dn20qurivo987pv07225.apps.googleusercontent.com',