feat:回调逻辑修改

This commit is contained in:
非凡主儿 2025-10-21 18:26:07 +08:00
parent 1f37369b3d
commit 79a63c4f76
5 changed files with 64 additions and 380 deletions

View File

@ -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>
);
}

View File

@ -1,17 +1,16 @@
'use client'; 'use client';
import React, { useState, useEffect } from 'react'; import React, { useState } from 'react';
import { Modal, Button, Spin, QRCode } from 'antd'; import { Modal, Button, Spin, QRCode } from 'antd';
import { import {
Youtube, Youtube,
Instagram, Instagram,
Share2, Share2,
Copy, Copy,
ExternalLink, ExternalLink,
Loader2 Loader2
} from 'lucide-react'; } from 'lucide-react';
import { baseUrl } from '@/lib/env'; import { shareApiUrl } from '@/lib/env';
import TwitterCallbackModal from './TwitterCallbackModal';
interface ShareModalProps { interface ShareModalProps {
/** 是否显示弹框 */ /** 是否显示弹框 */
@ -89,70 +88,6 @@ 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 [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 * ID
*/ */
@ -172,12 +107,7 @@ export default function ShareModal({ visible, onClose, project }: ShareModalProp
return; return;
} }
// Twitter 平台保存项目信息到 localStorage const response = await fetch(`${shareApiUrl}${apiPath}?user_id=${userId}`, {
if (platformName.toLowerCase().includes('twitter') || platformName.toLowerCase().includes('x')) {
localStorage.setItem('currentShareProject', JSON.stringify(project));
}
const response = await fetch(`${baseUrl}${apiPath}?user_id=${userId}`, {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -336,23 +266,6 @@ export default function ShareModal({ visible, onClose, project }: ShareModalProp
</div> </div>
</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> </Modal>
); );
} }

View File

@ -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>
);
}

View 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>
);
};

View File

@ -17,6 +17,7 @@ export interface EnvConfig {
javaUrl: string; javaUrl: string;
cutUrl: string; cutUrl: string;
cutUrlTo: string; cutUrlTo: string;
shareApiUrl: string; // 新增视频转发 URL 配置
// Google OAuth 配置 // Google OAuth 配置
googleClientId: string; googleClientId: string;
@ -52,6 +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',
// 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',
@ -90,6 +92,7 @@ export const {
javaUrl, javaUrl,
cutUrl, cutUrl,
cutUrlTo, cutUrlTo,
shareApiUrl, // 新增视频转发 URL 配置
// Google OAuth 配置 // Google OAuth 配置
googleClientId, googleClientId,