forked from 77media/video-flow
一键转发功能开发
This commit is contained in:
parent
098e51dde5
commit
e363aadf4c
22
app/api/video-share/x/auth/callback/page.tsx
Normal file
22
app/api/video-share/x/auth/callback/page.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function TwitterAuthCallbackPage() {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
// 此页面主要是为了 Next.js 路由捕获回调 URL。
|
||||
// 在 ShareModal 检测到参数后会打开 TwitterCallbackModal。
|
||||
// 重定向到主页面或相关仪表板页面。
|
||||
// 参数将由 ShareModal 接收处理。
|
||||
router.push('/movies?twitterCallback=true');
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-gray-900 text-white">
|
||||
<p>处理 Twitter 授权回调...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
316
components/common/ShareModal.tsx
Normal file
316
components/common/ShareModal.tsx
Normal file
@ -0,0 +1,316 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Button, Spin, QRCode } from 'antd';
|
||||
import {
|
||||
Youtube,
|
||||
Instagram,
|
||||
Share2,
|
||||
Copy,
|
||||
ExternalLink,
|
||||
Loader2
|
||||
} from 'lucide-react';
|
||||
import { shareApiUrl } from '@/lib/env';
|
||||
import TwitterCallbackModal from './TwitterCallbackModal';
|
||||
|
||||
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 [twitterCallbackVisible, setTwitterCallbackVisible] = useState(false);
|
||||
const [twitterCallbackParams, setTwitterCallbackParams] = useState<{
|
||||
state: string;
|
||||
code: string;
|
||||
} | null>(null);
|
||||
|
||||
/**
|
||||
* 检查 URL 参数并处理 Twitter 回调
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const state = urlParams.get('state');
|
||||
const code = urlParams.get('code');
|
||||
|
||||
// 检查是否是 Twitter 授权回调
|
||||
if (state && code && window.location.pathname.includes('/api/video-share/x/auth/callback')) {
|
||||
setTwitterCallbackParams({ state, code });
|
||||
setTwitterCallbackVisible(true);
|
||||
|
||||
// 清理 URL 参数
|
||||
const newUrl = window.location.pathname;
|
||||
window.history.replaceState({}, document.title, newUrl);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 获取用户ID
|
||||
*/
|
||||
const getUserId = () => {
|
||||
const currentUser = JSON.parse(localStorage.getItem('currentUser') || '{}');
|
||||
return currentUser.id || currentUser.userId;
|
||||
};
|
||||
|
||||
/**
|
||||
* 通用平台授权检查
|
||||
*/
|
||||
const checkPlatformAuth = async (apiPath: string, platformName: string) => {
|
||||
const userId = getUserId();
|
||||
|
||||
if (!userId) {
|
||||
console.error('用户ID不存在,请先登录');
|
||||
return;
|
||||
}
|
||||
|
||||
// Twitter 平台保存项目信息到 localStorage
|
||||
if (platformName.toLowerCase().includes('twitter') || platformName.toLowerCase().includes('x')) {
|
||||
localStorage.setItem('currentShareProject', JSON.stringify(project));
|
||||
}
|
||||
|
||||
const response = await fetch(`${shareApiUrl}${apiPath}?user_id=${userId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('token') || ''}`
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.successful && data.code === 0) {
|
||||
const { has_valid_token, auth_url, access_token } = data.data;
|
||||
|
||||
if (!has_valid_token) {
|
||||
// 需要用户授权,跳转到授权页面
|
||||
window.open(auth_url, '_blank');
|
||||
} else {
|
||||
// 用户已授权,直接使用access_token进行第三方登录
|
||||
console.log(`${platformName}用户已授权,access_token:`, access_token);
|
||||
// TODO: 实现视频上传逻辑
|
||||
}
|
||||
} else {
|
||||
console.error(`获取${platformName}授权信息失败:`, data.message);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理平台分享
|
||||
*/
|
||||
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') {
|
||||
alert('Instagram分享功能正在开发中,敬请期待!');
|
||||
} else if (platform.apiPath) {
|
||||
await checkPlatformAuth(platform.apiPath, platform.name);
|
||||
} else {
|
||||
console.log(`Share to ${platformId} not implemented yet`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Share to ${platformId} failed:`, error);
|
||||
} 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);
|
||||
console.log('视频链接已复制到剪贴板');
|
||||
// TODO: 显示复制成功提示
|
||||
} else {
|
||||
console.error('视频链接不存在');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('复制链接失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取视频链接用于生成二维码
|
||||
const getVideoUrl = () => {
|
||||
return project.final_video_url || project.final_simple_video_url || '';
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<Share2 className="w-5 h-5 text-white" />
|
||||
<span className="text-white font-medium">Share Video</span>
|
||||
</div>
|
||||
}
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
width={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',
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
|
||||
{/* Twitter 授权回调弹框 */}
|
||||
{twitterCallbackParams && (
|
||||
<TwitterCallbackModal
|
||||
visible={twitterCallbackVisible}
|
||||
onClose={() => {
|
||||
setTwitterCallbackVisible(false);
|
||||
setTwitterCallbackParams(null);
|
||||
}}
|
||||
project={project}
|
||||
urlParams={twitterCallbackParams}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
223
components/common/TwitterCallbackModal.tsx
Normal file
223
components/common/TwitterCallbackModal.tsx
Normal file
@ -0,0 +1,223 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Button, Spin, message } from 'antd';
|
||||
import { CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { shareApiUrl } from '@/lib/env';
|
||||
|
||||
interface TwitterCallbackModalProps {
|
||||
/** 是否显示弹框 */
|
||||
visible: boolean;
|
||||
/** 关闭弹框回调 */
|
||||
onClose: () => void;
|
||||
/** 项目信息 */
|
||||
project: {
|
||||
project_id: string;
|
||||
name?: string;
|
||||
final_video_url?: string;
|
||||
final_simple_video_url?: string;
|
||||
};
|
||||
/** URL 参数 */
|
||||
urlParams: {
|
||||
state: string;
|
||||
code: string;
|
||||
};
|
||||
}
|
||||
|
||||
enum CallbackStatus {
|
||||
LOADING = 'loading',
|
||||
SUCCESS = 'success',
|
||||
FAILED = 'failed'
|
||||
}
|
||||
|
||||
export default function TwitterCallbackModal({
|
||||
visible,
|
||||
onClose,
|
||||
project,
|
||||
urlParams
|
||||
}: TwitterCallbackModalProps) {
|
||||
const [callbackStatus, setCallbackStatus] = useState<CallbackStatus>(CallbackStatus.LOADING);
|
||||
const [callbackData, setCallbackData] = useState<any>(null);
|
||||
|
||||
/**
|
||||
* 处理 Twitter 授权回调
|
||||
*/
|
||||
const handleTwitterCallback = async () => {
|
||||
try {
|
||||
setCallbackStatus(CallbackStatus.LOADING);
|
||||
|
||||
// 从localStorage获取用户信息
|
||||
const currentUser = JSON.parse(localStorage.getItem('currentUser') || '{}');
|
||||
const userId = currentUser.id || currentUser.userId;
|
||||
|
||||
if (!userId) {
|
||||
throw new Error('用户ID不存在,请先登录');
|
||||
}
|
||||
|
||||
// 调用 Twitter 授权回调接口
|
||||
const response = await fetch(`${shareApiUrl}/api/video-share/x/auth/callback`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('token') || ''}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_id: userId,
|
||||
state: urlParams.state,
|
||||
code: urlParams.code,
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.successful && data.code === 0) {
|
||||
setCallbackData(data.data);
|
||||
setCallbackStatus(CallbackStatus.SUCCESS);
|
||||
message.success('Twitter 授权成功!');
|
||||
} else {
|
||||
throw new Error(data.message || 'Twitter 授权回调失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Twitter 授权回调失败:', error);
|
||||
setCallbackStatus(CallbackStatus.FAILED);
|
||||
message.error(`Twitter 授权失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时自动处理回调
|
||||
useEffect(() => {
|
||||
if (visible && urlParams.state && urlParams.code) {
|
||||
handleTwitterCallback();
|
||||
}
|
||||
}, [visible, urlParams.state, urlParams.code]);
|
||||
|
||||
/**
|
||||
* 重新尝试授权
|
||||
*/
|
||||
const handleRetry = () => {
|
||||
setCallbackStatus(CallbackStatus.LOADING);
|
||||
handleTwitterCallback();
|
||||
};
|
||||
|
||||
/**
|
||||
* 关闭弹框
|
||||
*/
|
||||
const handleClose = () => {
|
||||
setCallbackStatus(CallbackStatus.LOADING);
|
||||
setCallbackData(null);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-5 h-5 rounded-full bg-blue-400 flex items-center justify-center">
|
||||
<span className="text-white text-xs font-bold">X</span>
|
||||
</div>
|
||||
<span className="text-white font-medium">Twitter 授权回调</span>
|
||||
</div>
|
||||
}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
footer={null}
|
||||
width={480}
|
||||
className="twitter-callback-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',
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
{/* 项目信息 */}
|
||||
<div className="bg-black/20 backdrop-blur-sm rounded-lg p-4 border border-white/20">
|
||||
<h3 className="text-white font-medium text-sm mb-2">
|
||||
分享视频: {project.name || 'Unnamed Project'}
|
||||
</h3>
|
||||
<p className="text-white/50 text-xs">
|
||||
视频链接: {project.final_video_url || project.final_simple_video_url || 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 回调状态显示 */}
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
{callbackStatus === CallbackStatus.LOADING && (
|
||||
<>
|
||||
<Loader2 className="w-12 h-12 animate-spin text-blue-400" />
|
||||
<div className="text-center">
|
||||
<p className="text-white font-medium mb-1">正在处理 Twitter 授权...</p>
|
||||
<p className="text-white/60 text-sm">请稍候,正在验证授权信息</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{callbackStatus === CallbackStatus.SUCCESS && (
|
||||
<>
|
||||
<CheckCircleOutlined className="w-12 h-12 text-green-400" />
|
||||
<div className="text-center">
|
||||
<p className="text-white font-medium mb-1">授权成功!</p>
|
||||
<p className="text-white/60 text-sm mb-4">
|
||||
Twitter 授权已完成,可以开始分享视频
|
||||
</p>
|
||||
{callbackData && (
|
||||
<div className="bg-green-500/10 border border-green-500/20 rounded-lg p-3 mb-4">
|
||||
<p className="text-green-400 text-xs">
|
||||
授权 Token: {callbackData.access_token ? '已获取' : '未获取'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleClose}
|
||||
className="bg-blue-500 hover:bg-blue-600 border-blue-500"
|
||||
>
|
||||
完成
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{callbackStatus === CallbackStatus.FAILED && (
|
||||
<>
|
||||
<CloseCircleOutlined className="w-12 h-12 text-red-400" />
|
||||
<div className="text-center">
|
||||
<p className="text-white font-medium mb-1">授权失败</p>
|
||||
<p className="text-white/60 text-sm mb-4">
|
||||
Twitter 授权处理失败,请重试
|
||||
</p>
|
||||
<div className="flex gap-2 justify-center">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={handleRetry}
|
||||
className="bg-gray-600 hover:bg-gray-700 border-gray-600"
|
||||
>
|
||||
重试
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
onClick={handleClose}
|
||||
className="bg-gray-600 hover:bg-gray-700 border-gray-600"
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { Loader2, Download } from 'lucide-react';
|
||||
import { Loader2, Download, Share } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import './style/create-to-video2.css';
|
||||
|
||||
@ -14,7 +14,9 @@ import { motion } from 'framer-motion';
|
||||
import { Tooltip, Button } from 'antd';
|
||||
import { downloadVideo, downloadAllVideos, getFirstFrame } from '@/utils/tools';
|
||||
import { showDownloadOptionsModal } from '@/components/pages/work-flow/download-options-modal';
|
||||
import { post } from '@/api/request';
|
||||
import { post, get } from '@/api/request';
|
||||
import { shareApiUrl } from '@/lib/env';
|
||||
import ShareModal from '@/components/common/ShareModal';
|
||||
import Masonry from 'react-masonry-css';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
@ -35,6 +37,9 @@ export default function CreateToVideo2() {
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [userId, setUserId] = useState<number>(0);
|
||||
const [isLoadingDownloadBtn, setIsLoadingDownloadBtn] = useState(false);
|
||||
const [isLoadingShareBtn, setIsLoadingShareBtn] = useState(false);
|
||||
const [shareModalVisible, setShareModalVisible] = useState(false);
|
||||
const [selectedProject, setSelectedProject] = useState<any>(null);
|
||||
const masonryRef = useRef<any>(null);
|
||||
interface PreloadedData {
|
||||
page: number;
|
||||
@ -295,6 +300,12 @@ export default function CreateToVideo2() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleShareClick = (e: MouseEvent, project: MovieProject) => {
|
||||
e.stopPropagation();
|
||||
setSelectedProject(project);
|
||||
setShareModalVisible(true);
|
||||
};
|
||||
|
||||
const getPosterUrl = (project: MovieProject): string => {
|
||||
if (project.video_snapshot_url && project.video_snapshot_url.trim() !== '') {
|
||||
return project.video_snapshot_url;
|
||||
@ -360,9 +371,21 @@ export default function CreateToVideo2() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 下载按钮 右上角 */}
|
||||
{/* 转发和下载按钮 右上角 */}
|
||||
{(project.final_video_url || project.final_simple_video_url) && (
|
||||
<div className="absolute top-1 right-1">
|
||||
<div className="absolute top-1 right-1 flex items-center gap-1">
|
||||
{/* 转发按钮 */}
|
||||
<Tooltip placement="top" title="Share">
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
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) => handleShareClick(e, project)}
|
||||
>
|
||||
<Share className="w-4 h-4 text-white" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{/* 下载按钮 */}
|
||||
<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)}>
|
||||
{isLoadingDownloadBtn ? <Loader2 className="w-4 h-4 animate-spin text-white" /> : <Download className="w-4 h-4 text-white" />}
|
||||
@ -453,6 +476,18 @@ export default function CreateToVideo2() {
|
||||
{!isLoading &&
|
||||
<ChatInputBox noData={episodeList.length === 0} />
|
||||
}
|
||||
|
||||
{/* 分享弹框 */}
|
||||
{selectedProject && (
|
||||
<ShareModal
|
||||
visible={shareModalVisible}
|
||||
onClose={() => {
|
||||
setShareModalVisible(false);
|
||||
setSelectedProject(null);
|
||||
}}
|
||||
project={selectedProject}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
14
lib/env.ts
14
lib/env.ts
@ -17,6 +17,7 @@ export interface EnvConfig {
|
||||
javaUrl: string;
|
||||
cutUrl: string;
|
||||
cutUrlTo: string;
|
||||
shareApiUrl: string;
|
||||
|
||||
// Google OAuth 配置
|
||||
googleClientId: string;
|
||||
@ -52,6 +53,7 @@ export const getEnvConfig = (): EnvConfig => {
|
||||
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',
|
||||
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',
|
||||
|
||||
// Google OAuth 配置
|
||||
googleClientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || '847079918888-o1nne8d3ij80dn20qurivo987pv07225.apps.googleusercontent.com',
|
||||
@ -90,6 +92,7 @@ export const {
|
||||
javaUrl,
|
||||
cutUrl,
|
||||
cutUrlTo,
|
||||
shareApiUrl,
|
||||
|
||||
// Google OAuth 配置
|
||||
googleClientId,
|
||||
@ -155,6 +158,10 @@ export const validateEnvConfig = (): { isValid: boolean; errors: string[] } => {
|
||||
errors.push('NEXT_PUBLIC_JAVA_URL is required');
|
||||
}
|
||||
|
||||
if (!shareApiUrl) {
|
||||
errors.push('NEXT_PUBLIC_SHARE_API_URL is required');
|
||||
}
|
||||
|
||||
if (!googleClientId) {
|
||||
errors.push('NEXT_PUBLIC_GOOGLE_CLIENT_ID is required');
|
||||
}
|
||||
@ -172,6 +179,12 @@ export const validateEnvConfig = (): { isValid: boolean; errors: string[] } => {
|
||||
errors.push('NEXT_PUBLIC_JAVA_URL must be a valid URL');
|
||||
}
|
||||
|
||||
try {
|
||||
new URL(shareApiUrl);
|
||||
} catch {
|
||||
errors.push('NEXT_PUBLIC_SHARE_API_URL must be a valid URL');
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
@ -189,6 +202,7 @@ export const logEnvConfig = (): void => {
|
||||
javaUrl,
|
||||
cutUrl,
|
||||
cutUrlTo,
|
||||
shareApiUrl,
|
||||
googleClientId,
|
||||
googleRedirectUri: getGoogleRedirectUri(),
|
||||
gaEnabled,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user