forked from 77media/video-flow
feat:回调逻辑修改
This commit is contained in:
parent
1f37369b3d
commit
79a63c4f76
@ -1,71 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import TwitterCallbackModal from '@/components/common/TwitterCallbackModal';
|
||||
|
||||
export default function TwitterAuthCallbackPage() {
|
||||
const router = useRouter();
|
||||
const [callbackParams, setCallbackParams] = useState<{ state: string; code: string } | null>(null);
|
||||
const [project, setProject] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// 解析回调参数
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const state = searchParams.get('state');
|
||||
const code = searchParams.get('code');
|
||||
|
||||
if (state && code) {
|
||||
setCallbackParams({ state, code });
|
||||
|
||||
// 从 localStorage 获取项目信息
|
||||
const storedProject = localStorage.getItem('currentShareProject');
|
||||
if (storedProject) {
|
||||
try {
|
||||
const parsedProject = JSON.parse(storedProject);
|
||||
setProject(parsedProject);
|
||||
} catch (error) {
|
||||
console.error('解析项目信息失败:', error);
|
||||
setProject({ project_id: '', name: '未知项目', final_video_url: '' });
|
||||
}
|
||||
} else {
|
||||
setProject({ project_id: '', name: '未知项目', final_video_url: '' });
|
||||
}
|
||||
|
||||
// 清理 URL 参数
|
||||
const cleanUrl = window.location.pathname;
|
||||
window.history.replaceState({}, document.title, cleanUrl);
|
||||
} else {
|
||||
// 如果没有参数,重定向到主页面
|
||||
router.push('/movies');
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
const handleCloseModal = () => {
|
||||
// 清理 localStorage 中的临时数据
|
||||
try {
|
||||
localStorage.removeItem('twitterAuthCallbackPayload');
|
||||
localStorage.removeItem('twitterAuthCallbackFlag');
|
||||
} catch (error) {
|
||||
console.error('清理 localStorage 失败:', error);
|
||||
}
|
||||
setCallbackParams(null);
|
||||
router.push('/movies');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-gray-900 text-white">
|
||||
{callbackParams && project && (
|
||||
<TwitterCallbackModal
|
||||
visible={true}
|
||||
onClose={handleCloseModal}
|
||||
project={project}
|
||||
urlParams={callbackParams}
|
||||
/>
|
||||
)}
|
||||
{!callbackParams && (
|
||||
<p>处理 Twitter 授权回调...</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,17 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Modal, Button, Spin, QRCode } from 'antd';
|
||||
import {
|
||||
Youtube,
|
||||
Instagram,
|
||||
Share2,
|
||||
Share2,
|
||||
Copy,
|
||||
ExternalLink,
|
||||
Loader2
|
||||
} from 'lucide-react';
|
||||
import { baseUrl } from '@/lib/env';
|
||||
import TwitterCallbackModal from './TwitterCallbackModal';
|
||||
import { shareApiUrl } from '@/lib/env';
|
||||
|
||||
interface ShareModalProps {
|
||||
/** 是否显示弹框 */
|
||||
@ -89,70 +88,6 @@ const sharePlatforms: SharePlatform[] = [
|
||||
|
||||
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');
|
||||
|
||||
// 当前就在回调路径(同窗口授权)
|
||||
if (state && code && window.location.pathname.includes('/api/video-share/x/auth/callback')) {
|
||||
setTwitterCallbackParams({ state, code });
|
||||
setTwitterCallbackVisible(true);
|
||||
const newUrl = window.location.pathname;
|
||||
window.history.replaceState({}, document.title, newUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
// 新窗口授权,回到非回调路径,比如 /movies?twitterCallback=true
|
||||
const hasFlag = urlParams.get('twitterCallback') === 'true';
|
||||
const cached = localStorage.getItem('twitterAuthCallbackPayload');
|
||||
if (hasFlag && cached) {
|
||||
try {
|
||||
const parsed = JSON.parse(cached);
|
||||
if (parsed?.state && parsed?.code) {
|
||||
setTwitterCallbackParams({ state: parsed.state, code: parsed.code });
|
||||
setTwitterCallbackVisible(true);
|
||||
}
|
||||
} catch {
|
||||
|
||||
}
|
||||
// 清理 URL 中的 twitterCallback 标记
|
||||
const cleanUrl = window.location.pathname;
|
||||
window.history.replaceState({}, document.title, cleanUrl);
|
||||
}
|
||||
|
||||
// 监听 storage 事件(跨窗口通知)
|
||||
const onStorage = (e: StorageEvent) => {
|
||||
if (e.key === 'twitterAuthCallbackFlag') {
|
||||
const cachedPayload = localStorage.getItem('twitterAuthCallbackPayload');
|
||||
if (cachedPayload) {
|
||||
try {
|
||||
const parsed = JSON.parse(cachedPayload);
|
||||
if (parsed?.state && parsed?.code) {
|
||||
setTwitterCallbackParams({ state: parsed.state, code: parsed.code });
|
||||
setTwitterCallbackVisible(true);
|
||||
}
|
||||
} catch {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
window.addEventListener('storage', onStorage);
|
||||
return () => window.removeEventListener('storage', onStorage);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 获取用户ID
|
||||
*/
|
||||
@ -172,12 +107,7 @@ export default function ShareModal({ visible, onClose, project }: ShareModalProp
|
||||
return;
|
||||
}
|
||||
|
||||
// Twitter 平台保存项目信息到 localStorage
|
||||
if (platformName.toLowerCase().includes('twitter') || platformName.toLowerCase().includes('x')) {
|
||||
localStorage.setItem('currentShareProject', JSON.stringify(project));
|
||||
}
|
||||
|
||||
const response = await fetch(`${baseUrl}${apiPath}?user_id=${userId}`, {
|
||||
const response = await fetch(`${shareApiUrl}${apiPath}?user_id=${userId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@ -336,23 +266,6 @@ export default function ShareModal({ visible, onClose, project }: ShareModalProp
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Twitter 授权回调弹框 */}
|
||||
{twitterCallbackParams && (
|
||||
<TwitterCallbackModal
|
||||
visible={twitterCallbackVisible}
|
||||
onClose={() => {
|
||||
setTwitterCallbackVisible(false);
|
||||
setTwitterCallbackParams(null);
|
||||
// 关闭时清理临时缓存
|
||||
try {
|
||||
localStorage.removeItem('twitterAuthCallbackPayload');
|
||||
localStorage.removeItem('twitterAuthCallbackFlag');
|
||||
} catch {}
|
||||
}}
|
||||
project={project}
|
||||
urlParams={twitterCallbackParams}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,218 +0,0 @@
|
||||
'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 { baseUrl } 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(`${baseUrl}/api/video-share/x/auth/callback?state=${encodeURIComponent(urlParams.state)}&code=${encodeURIComponent(urlParams.code)}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('token') || ''}`
|
||||
}
|
||||
});
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
57
components/common/VideoShareForm.tsx
Normal file
57
components/common/VideoShareForm.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
|
||||
export const VideoShareForm: React.FC<{ onCancel: () => void; onShare: (videoUrl: string, videoName: string) => void }> = ({ onCancel, onShare }) => {
|
||||
const [videoUrl, setVideoUrl] = useState('');
|
||||
const [videoName, setVideoName] = useState('');
|
||||
|
||||
const handleShare = () => {
|
||||
onShare(videoUrl, videoName);
|
||||
};
|
||||
|
||||
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">
|
||||
<h2 data-alt="form-title" className="text-2xl font-bold mb-4 text-slate-800 dark:text-slate-200">转发视频</h2>
|
||||
<div data-alt="form-fields" className="space-y-4">
|
||||
<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>
|
||||
<Input
|
||||
id="videoUrl"
|
||||
value={videoUrl}
|
||||
onChange={(e) => setVideoUrl(e.target.value)}
|
||||
placeholder="请输入视频链接"
|
||||
className="bg-white/50 dark:bg-slate-800/50 border-slate-300 dark:border-slate-700 text-slate-900 dark:text-slate-100"
|
||||
/>
|
||||
</div>
|
||||
<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>
|
||||
<Input
|
||||
id="videoName"
|
||||
value={videoName}
|
||||
onChange={(e) => setVideoName(e.target.value)}
|
||||
placeholder="请输入视频名称"
|
||||
className="bg-white/50 dark:bg-slate-800/50 border-slate-300 dark:border-slate-700 text-slate-900 dark:text-slate-100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div data-alt="form-actions" className="mt-6 flex justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onCancel}
|
||||
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"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleShare}
|
||||
data-alt="share-button"
|
||||
className="bg-blue-500 hover:bg-blue-600 text-white"
|
||||
>
|
||||
一键转发
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -17,6 +17,7 @@ export interface EnvConfig {
|
||||
javaUrl: string;
|
||||
cutUrl: string;
|
||||
cutUrlTo: string;
|
||||
shareApiUrl: string; // 新增视频转发 URL 配置
|
||||
|
||||
// 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, // 新增视频转发 URL 配置
|
||||
|
||||
// Google OAuth 配置
|
||||
googleClientId,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user