video-flow-b/components/common/ShareModal.tsx
2025-10-22 18:16:35 +08:00

352 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import React, { useState, useEffect } from 'react';
import { Modal, Button, Spin, QRCode, message } from 'antd';
import {
Youtube,
Instagram,
Share2,
Copy,
ExternalLink,
Loader2
} from 'lucide-react';
import { shareApiUrl } from '@/lib/env';
import { VideoShareForm } from './VideoShareForm';
interface ShareModalProps {
/** 是否显示弹框 */
visible: boolean;
/** 关闭弹框回调 */
onClose: () => void;
/** 项目信息 */
project: {
project_id: string;
name?: string;
final_video_url?: string;
final_simple_video_url?: string;
};
}
interface SharePlatform {
id: string;
name: string;
icon: React.ReactNode;
color: string;
description: string;
apiPath: string | null;
}
const sharePlatforms: SharePlatform[] = [
{
id: 'youtube',
name: 'YouTube',
icon: <Youtube className="w-6 h-6" />,
color: 'text-red-500',
description: 'Share to YouTube',
apiPath: '/api/video-share/youtube/auth/url'
},
{
id: 'tiktok',
name: 'TikTok',
icon: <Share2 className="w-6 h-6" />,
color: 'text-black',
description: 'Share to TikTok',
apiPath: '/api/video-share/tiktok/auth/url'
},
{
id: 'instagram',
name: 'Instagram',
icon: <Instagram className="w-6 h-6" />,
color: 'text-pink-500',
description: 'Share to Instagram',
apiPath: null // 开发中
},
{
id: 'reddit',
name: 'Reddit',
icon: <ExternalLink className="w-6 h-6" />,
color: 'text-orange-500',
description: 'Share to Reddit',
apiPath: '/api/video-share/reddit/auth/url'
},
{
id: 'twitter',
name: 'X (Twitter)',
icon: <ExternalLink className="w-6 h-6" />,
color: 'text-blue-400',
description: 'Share to X (Twitter)',
apiPath: '/api/video-share/x/auth/url'
},
{
id: 'copy',
name: 'Copy Link',
icon: <Copy className="w-6 h-6" />,
color: 'text-gray-400',
description: 'Copy video link to clipboard',
apiPath: null // 本地功能
}
];
export default function ShareModal({ visible, onClose, project }: ShareModalProps) {
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
*/
const getUserId = () => {
const currentUser = JSON.parse(localStorage.getItem('currentUser') || '{}');
return currentUser.id || currentUser.userId;
};
/**
* 通用平台授权检查
*/
const checkPlatformAuth = async (apiPath: string, platformName: string, platformId: string) => {
const userId = getUserId();
if (!userId) {
message.error('用户ID不存在请先登录');
return;
}
const response = await fetch(`${shareApiUrl}${apiPath}?user_id=${userId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token') || ''}`,
'ngrok-skip-browser-warning': 'true'
}
});
const data = await response.json();
if (data.successful && data.code === 0) {
const { has_valid_token, auth_url } = data.data;
if (!has_valid_token) {
// 需要用户授权,跳转到授权页面
window.open(auth_url, '_blank');
message.info('请在新窗口完成授权');
} else {
// 用户已授权,直接显示上传表单
setSelectedPlatform(platformId);
setAuthError(null);
setShowShareForm(true);
message.success(`${platformName} 已授权,可以直接上传视频`);
}
} else {
message.error(data.message || `获取${platformName}授权信息失败`);
}
};
/**
* 处理平台分享
*/
const handlePlatformShare = async (platformId: string) => {
setLoadingPlatform(platformId);
try {
const platform = sharePlatforms.find(p => p.id === platformId);
if (!platform) return;
if (platformId === 'copy') {
await handleCopyLink();
} else if (platformId === 'instagram') {
message.info('Instagram分享功能正在开发中敬请期待');
} else if (platform.apiPath) {
await checkPlatformAuth(platform.apiPath, platform.name, platformId);
} else {
console.log(`Share to ${platformId} not implemented yet`);
}
} catch (error) {
console.error(`Share to ${platformId} failed:`, error);
message.error(`操作失败:${error instanceof Error ? error.message : '未知错误'}`);
} finally {
setLoadingPlatform(null);
}
};
/**
* 复制链接处理
*/
const handleCopyLink = async () => {
try {
const videoUrl = project.final_video_url || project.final_simple_video_url;
if (videoUrl) {
await navigator.clipboard.writeText(videoUrl);
message.success('视频链接已复制到剪贴板');
} else {
message.error('视频链接不存在');
}
} catch (error) {
console.error('复制链接失败:', error);
message.error('复制链接失败,请手动复制');
}
};
// 获取视频链接用于生成二维码
const getVideoUrl = () => {
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 (
<Modal
title={
<div className="flex items-center gap-2">
<Share2 className="w-5 h-5 text-white" />
<span className="text-white font-medium">
{showShareForm ? `分享到 ${selectedPlatform}` : 'Share Video'}
</span>
</div>
}
open={visible}
onCancel={onClose}
footer={null}
width={showShareForm ? 500 : 720}
className="share-modal"
styles={{
content: {
backgroundColor: 'rgba(27, 27, 27, 0.8)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(255, 255, 255, 0.1)',
},
header: {
backgroundColor: 'rgba(27, 27, 27, 0.6)',
backdropFilter: 'blur(20px)',
borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
},
body: {
backgroundColor: 'transparent',
}
}}
>
{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="bg-white/90 backdrop-blur-sm p-4 rounded-lg border border-white/20">
<QRCode
value={getVideoUrl()}
size={160}
color="#000000"
bgColor="#ffffff"
errorLevel="M"
/>
</div>
</div>
{/* 右侧:平台选择区域 */}
<div className="flex-1 space-y-4">
<div className="text-center mb-4">
<h4 className="text-white font-medium text-sm mb-2">Share to Platform</h4>
<p className="text-white/60 text-xs">
Choose a platform to share your video
</p>
</div>
{/* 分享平台选择 */}
<div className="grid grid-cols-2 gap-3">
{sharePlatforms.map((platform) => (
<Button
key={platform.id}
type="text"
className={`
h-16 flex flex-col items-center justify-center gap-2
bg-black/20 backdrop-blur-sm hover:bg-white/10 border border-white/20
transition-all duration-300 rounded-lg
${loadingPlatform === platform.id ? 'opacity-50' : ''}
`}
disabled={loadingPlatform !== null}
onClick={() => handlePlatformShare(platform.id)}
>
<div className={`${platform.color} ${loadingPlatform === platform.id ? 'opacity-50' : ''}`}>
{loadingPlatform === platform.id ? (
<Loader2 className="w-6 h-6 animate-spin" />
) : (
platform.icon
)}
</div>
<span className="text-white text-xs font-medium">
{platform.name}
</span>
</Button>
))}
</div>
</div>
</div>
)}
</Modal>
);
}