diff --git a/.env.development b/.env.development index e37d8cc..215df85 100644 --- a/.env.development +++ b/.env.development @@ -1,5 +1,5 @@ - -NEXT_PUBLIC_JAVA_URL = https://77.app.java.auth.qikongjian.com +NEXT_PUBLIC_JAVA_URL = https://auth.test.movieflow.ai +# NEXT_PUBLIC_JAVA_URL = https://77.app.java.auth.qikongjian.com NEXT_PUBLIC_BASE_URL = https://77.smartvideo.py.qikongjian.com NEXT_PUBLIC_CUT_URL = https://77.smartcut.py.qikongjian.com diff --git a/api/script_episode.ts b/api/script_episode.ts index cc074fc..490333b 100644 --- a/api/script_episode.ts +++ b/api/script_episode.ts @@ -69,13 +69,14 @@ interface ListMovieProjectsParams { per_page: number; } -interface MovieProject { +export interface MovieProject { project_id: string; name: string; status: string; step: string; final_video_url: string; final_simple_video_url: string; + video_urls: string; last_message: string; updated_at: string; created_at: string; diff --git a/app/activate/page.tsx b/app/activate/page.tsx index d342139..824beff 100644 --- a/app/activate/page.tsx +++ b/app/activate/page.tsx @@ -1,7 +1,8 @@ "use client"; + import { post } from "@/api/request"; -import { useSearchParams } from "next/navigation"; -import React, { useEffect, useState } from "react"; +import { useSearchParams, useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; import { CheckCircle, XCircle, Loader2 } from "lucide-react"; export default function Activate() { @@ -21,10 +22,12 @@ export default function Activate() { * @param {string} t - Verification token */ function ConfirmEmail({ t }: { t: string }) { + const router = useRouter(); const [status, setStatus] = useState<"loading" | "success" | "error">( "loading" ); const [message, setMessage] = useState(""); + const [countdown, setCountdown] = useState(3); useEffect(() => { if (!t) { @@ -35,18 +38,36 @@ function ConfirmEmail({ t }: { t: string }) { post(`/auth/activate`, { t: t, }).then((res:any) => { - console.log('res', res) setStatus("success"); - setMessage( - "Your registration has been verified. Please return to the official website to log in." - ); + setMessage( + "Your registration has been verified. Redirecting to login page..." + ); }).catch((err:any) => { - console.log('err', err) setStatus("error"); setMessage("Verification failed. Please try again."); }); }, [t]); + /** + * Handle countdown and redirect to login page + */ + useEffect(() => { + if (status === "success") { + const timer = setInterval(() => { + setCountdown((prev: number) => { + if (prev <= 1) { + clearInterval(timer); + router.push("/login"); + return 0; + } + return prev - 1; + }); + }, 1000); + + return () => clearInterval(timer); + } + }, [status, router]); + const renderContent = () => { switch (status) { case "loading": @@ -77,6 +98,18 @@ function ConfirmEmail({ t }: { t: string }) { Verification Successful

{message}

+
+

+ Redirecting to login page in {countdown} seconds... +

+
+ ); diff --git a/app/api/server-setting/find_by_code/route.ts b/app/api/server-setting/find_by_code/route.ts index dd178ae..6f500b2 100644 --- a/app/api/server-setting/find_by_code/route.ts +++ b/app/api/server-setting/find_by_code/route.ts @@ -59,6 +59,23 @@ export async function POST(request: NextRequest) { updated_at: new Date().toISOString().slice(0, 19) }; break; + + case 'video_modification': + // 视频修改功能配置 - 控制视频编辑笔图标显示 + // 可以通过查询参数 ?show=false 来测试隐藏功能 + const url = new URL(request.url); + const showParam = url.searchParams.get('show'); + const showValue = showParam !== null ? showParam === 'true' : true; // 默认显示 + + responseData = { + id: 9, + code: 'video_modification', + value: `{\n "show": ${showValue}\n}`, + note: '视频修改功能开关', + updated_at: new Date().toISOString().slice(0, 19) + }; + console.log('📋 video_modification配置:', { showParam, showValue, value: responseData.value }); + break; default: // 默认返回空配置 diff --git a/app/share/page.tsx b/app/share/page.tsx index d7a454f..ae5ad57 100644 --- a/app/share/page.tsx +++ b/app/share/page.tsx @@ -197,36 +197,7 @@ export default function SharePage(): JSX.Element {

Invite friends to join and earn rewards.

- - {/* Section 1: Invite Flow */} -
-

Invitation Flow

-
    -
  1. -
    - Step 1 - Share -
    -

    Copy your invitation link and share it with friends.

    -
  2. -
  3. -
    - Step 2 - Register -
    -

    Friends click the link and register directly.

    -
  4. -
  5. -
    - Step 3 - Reward -
    -

    You both receive rewards after your friend activates their account.

    -
  6. -
-
- - {/* Section 2: My Invitation Link */} + {/* Section 1: My Invitation Link */}
@@ -277,6 +248,74 @@ export default function SharePage(): JSX.Element {
+ {/* Section 2: Invite Flow - Two Columns (Left: Steps, Right: Rules) */} +
+
+ {/* Left: Steps */} +
+

Invitation Flow

+
    +
  1. +
    + Step 1 + Share +
    +

    Copy your invitation link and share it with friends.

    +
  2. +
  3. +
    + Step 2 + Register +
    +

    Friends click the link and register directly.

    +
  4. +
  5. +
    + Step 3 + Reward +
    +

    You both receive rewards after your friend activates their account.

    +
  6. +
+
+ {/* Right: Rules */} +
+

MovieFlow Credits Rewards Program

+
+

Welcome to MovieFlow! Our Credits Program is designed to reward your growth and contributions. Credits can be redeemed for premium templates, effects, and membership time.

+ +
+

How to Earn Credits?

+ +
+

Welcome Bonus

+

All new users receive a bonus of 500 credits upon successful registration!

+
+ +
+

Invite & Earn

+

Invite friends to join using your unique referral link. Both you and your friend will get 500 credits once they successfully sign up.

+ +
+

If your invited friend completes their first purchase, you will receive a bonus equal to 20% of the credits they earn from that purchase.

+
+
+ +
+

Daily Login

+

Starting the day after registration, log in daily to claim 100 credits.

+

This reward can be claimed for 7 consecutive days.

+ +
+

Please note: Daily login credits will reset automatically on the 8th day, so remember to use them in time!

+
+
+
+
+ +
+
+
{/* Section 3: Invite Records */}
diff --git a/app/test-server-config/page.tsx b/app/test-server-config/page.tsx new file mode 100644 index 0000000..9d4b9e6 --- /dev/null +++ b/app/test-server-config/page.tsx @@ -0,0 +1,112 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { isVideoModificationEnabled, isGoogleLoginEnabled } from '@/lib/server-config'; + +export default function TestServerConfigPage() { + const [ssoStatus, setSsoStatus] = useState(null); + const [videoModStatus, setVideoModStatus] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const testConfigs = async () => { + setLoading(true); + setError(null); + + try { + console.log('🧪 开始测试服务器配置...'); + + // 测试SSO配置 + const ssoEnabled = await isGoogleLoginEnabled(); + console.log('📋 SSO配置结果:', ssoEnabled); + setSsoStatus(ssoEnabled); + + // 测试视频修改配置 + const videoModEnabled = await isVideoModificationEnabled(); + console.log('📋 视频修改配置结果:', videoModEnabled); + setVideoModStatus(videoModEnabled); + + console.log('✅ 所有配置测试完成'); + } catch (err) { + console.error('❌ 配置测试失败:', err); + setError(err instanceof Error ? err.message : '未知错误'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + testConfigs(); + }, []); + + return ( +
+
+

服务器配置测试页面

+ +
+

配置状态

+ +
+
+ Google登录 (sso_config): + + {ssoStatus === null ? '检测中...' : ssoStatus ? '启用' : '禁用'} + +
+ +
+ 视频修改 (video_modification): + + {videoModStatus === null ? '检测中...' : videoModStatus ? '启用' : '禁用'} + +
+
+ + {error && ( +
+

错误:

+

{error}

+
+ )} + +
+ + + +
+
+ +
+

API测试信息

+
+

SSO API: POST /api/server-setting/find_by_code {"{ code: 'sso_config' }"}

+

视频修改API: POST /api/server-setting/find_by_code {"{ code: 'video_modification' }"}

+

预期响应格式: {"{ code: 0, successful: true, data: { value: '{\"show\": true}' } }"}

+
+
+ +
+

请打开浏览器开发者工具查看详细的API调用日志

+
+
+
+ ); +} diff --git a/components/ChatInputBox/ChatInputBox.tsx b/components/ChatInputBox/ChatInputBox.tsx index 763b7c1..04e219e 100644 --- a/components/ChatInputBox/ChatInputBox.tsx +++ b/components/ChatInputBox/ChatInputBox.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect, useRef, useCallback } from "react"; import { ChevronDown, ChevronUp, @@ -103,6 +103,33 @@ const debounce = (func: Function, wait: number) => { */ export function ChatInputBox({ noData }: { noData: boolean }) { const { isMobile, isDesktop } = useDeviceType(); + + // 模板快捷入口拖动相关状态 + const templateScrollRef = useRef(null); + const [isTemplateDragging, setIsTemplateDragging] = useState(false); + const [templateStartX, setTemplateStartX] = useState(0); + const [templateScrollLeft, setTemplateScrollLeft] = useState(0); + + // 模板快捷入口拖动事件处理 + const handleTemplateMouseDown = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + setIsTemplateDragging(true); + setTemplateStartX(e.pageX - templateScrollRef.current!.offsetLeft); + setTemplateScrollLeft(templateScrollRef.current!.scrollLeft); + }, []); + + const handleTemplateMouseMove = useCallback((e: React.MouseEvent) => { + if (!isTemplateDragging) return; + e.preventDefault(); + const x = e.pageX - templateScrollRef.current!.offsetLeft; + const walk = (x - templateStartX) * 2; + templateScrollRef.current!.scrollLeft = templateScrollLeft - walk; + }, [isTemplateDragging, templateStartX, templateScrollLeft]); + + const handleTemplateMouseUp = useCallback(() => { + setIsTemplateDragging(false); + }, []); + // 控制面板展开/收起状态 const [isExpanded, setIsExpanded] = useState(false); @@ -148,7 +175,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) { language: "english", videoDuration: "unlimited", expansion_mode: true, - aspect_ratio: "VIDEO_ASPECT_RATIO_LANDSCAPE", + aspect_ratio: isMobile ? "VIDEO_ASPECT_RATIO_PORTRAIT" : "VIDEO_ASPECT_RATIO_LANDSCAPE", }); // 从 localStorage 初始化配置 @@ -163,19 +190,38 @@ export function ChatInputBox({ noData }: { noData: boolean }) { language: parsed.language || "english", videoDuration: parsed.videoDuration || "unlimited", expansion_mode: typeof parsed.expansion_mode === 'boolean' ? parsed.expansion_mode : true, - aspect_ratio: parsed.aspect_ratio || "VIDEO_ASPECT_RATIO_LANDSCAPE", + aspect_ratio: parsed.aspect_ratio || (isMobile ? "VIDEO_ASPECT_RATIO_PORTRAIT" : "VIDEO_ASPECT_RATIO_LANDSCAPE"), }); } catch (error) { console.warn('解析保存的配置失败,使用默认配置:', error); } } - }, []); + }, [isMobile]); + + // 跟踪用户是否手动修改过宽高比 + const [hasUserChangedAspectRatio, setHasUserChangedAspectRatio] = useState(false); + + // 监听设备类型变化,仅在用户未手动修改时动态调整默认宽高比 + useEffect(() => { + if (!hasUserChangedAspectRatio) { + setConfigOptions(prev => ({ + ...prev, + aspect_ratio: isMobile ? "VIDEO_ASPECT_RATIO_PORTRAIT" : "VIDEO_ASPECT_RATIO_LANDSCAPE" + })); + } + }, [isMobile, hasUserChangedAspectRatio]); const onConfigChange = (key: K, value: ConfigOptions[K]) => { setConfigOptions((prev: ConfigOptions) => ({ ...prev, [key]: value, })); + + // 如果用户手动修改了宽高比,标记为已修改 + if (key === 'aspect_ratio') { + setHasUserChangedAspectRatio(true); + } + if (key === 'videoDuration') { // 当选择 8s 时,强制关闭剧本扩展并禁用开关 if (value === '8s') { @@ -510,7 +556,14 @@ export function ChatInputBox({ noData }: { noData: boolean }) { {/* 第三行:模板快捷入口水平滚动 */}
-
+
{isTemplateLoading && (!templateStoryList || templateStoryList.length === 0) ? ( // 骨架屏:若正在加载且没有数据 Array.from({ length: 6 }).map((_, idx) => ( @@ -525,7 +578,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) { {showTip && ( -
+

Sign-in Rules

• Daily sign-in earns 100 credits

• Credits are valid for 7 days

• Expired credits will be automatically cleared

-
+
)}
- +
-
+
Earned Credits
-
+
{signinData.credits || 0}
@@ -124,9 +130,10 @@ export default function SigninPage() { {/* Sign-in button */} {isLoadingSubscription ? "Loading..." @@ -568,10 +576,8 @@ export function TopBar({ collapsed, isDesktop=true }: { collapsed: boolean, isDe data-alt="signin-modal" > -
- -
- + +
); diff --git a/components/pages/create-to-video2.tsx b/components/pages/create-to-video2.tsx index 6147b1c..e71f762 100644 --- a/components/pages/create-to-video2.tsx +++ b/components/pages/create-to-video2.tsx @@ -5,13 +5,14 @@ import { Loader2, Download } from 'lucide-react'; import { useRouter } from 'next/navigation'; import './style/create-to-video2.css'; -import { getScriptEpisodeListNew } from "@/api/script_episode"; +import { getScriptEpisodeListNew, MovieProject } from "@/api/script_episode"; import { ChatInputBox } from '@/components/ChatInputBox/ChatInputBox'; import cover_image1 from '@/public/assets/cover_image3.jpg'; import { motion } from 'framer-motion'; import { Tooltip, Button } from 'antd'; import { downloadVideo, getFirstFrame } from '@/utils/tools'; -import LazyLoad from "react-lazyload"; +import Masonry from 'react-masonry-css'; +import debounce from 'lodash/debounce'; @@ -19,35 +20,75 @@ import LazyLoad from "react-lazyload"; export default function CreateToVideo2() { const router = useRouter(); - const [episodeList, setEpisodeList] = useState([]); + const [episodeList, setEpisodeList] = useState([]); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); - const [perPage] = useState(12); + const [perPage] = useState(28); const [isLoading, setIsLoading] = useState(false); const [hasMore, setHasMore] = useState(true); const [isLoadingMore, setIsLoadingMore] = useState(false); + const [isPreloading, setIsPreloading] = useState(false); const scrollContainerRef = useRef(null); const [userId, setUserId] = useState(0); const [isLoadingDownloadBtn, setIsLoadingDownloadBtn] = useState(false); + const masonryRef = useRef(null); + interface PreloadedData { + page: number; + data: { + code: number; + data: { + movie_projects: MovieProject[]; + total_pages: number; + }; + }; + } + + const preloadedDataRef = useRef(null); // 添加一个 ref 来跟踪当前正在加载的页码 const loadingPageRef = useRef(null); // 在客户端挂载后读取localStorage - // 监听滚动事件,实现无限加载 - // 修改滚动处理函数,添加节流 + // 预加载下一页数据 + const preloadNextPage = async (userId: number, page: number) => { + if (isPreloading || !hasMore || page > totalPages) return; + + setIsPreloading(true); + try { + const response = await fetchEpisodeData(userId, page); + if (response.code === 0) { + preloadedDataRef.current = { + page, + data: response + }; + } + } catch (error) { + console.error('Failed to preload next page:', error); + } finally { + setIsPreloading(false); + } + }; + + // 监听滚动事件,实现无限加载和预加载 const handleScroll = useCallback(() => { if (!scrollContainerRef.current || !hasMore || isLoadingMore || isLoading) return; const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.current; - if (scrollHeight - scrollTop - clientHeight < 100) { - // 直接使用 currentPage,不再使用 setCurrentPage 的回调 + const scrollPercentage = (scrollTop + clientHeight) / scrollHeight; + + // 在滚动到 30% 时预加载下一页 + // if (scrollPercentage > 0.30 && !isPreloading && currentPage < totalPages) { + // preloadNextPage(userId, currentPage + 1); + // } + + // 在滚动到 70% 时加载下一页 + if (scrollPercentage > 0.7) { const nextPage = currentPage + 1; if (nextPage <= totalPages) { getEpisodeList(userId, nextPage, true); } } - }, [hasMore, isLoadingMore, isLoading, totalPages, userId, currentPage]); + }, [hasMore, isLoadingMore, isLoading, totalPages, userId, currentPage, isPreloading]); useEffect(() => { if (typeof window !== 'undefined') { @@ -67,6 +108,16 @@ export default function CreateToVideo2() { } }, [handleScroll]); + // 获取剧集列表数据 + const fetchEpisodeData = async (userId: number, page: number) => { + const params = { + user_id: String(userId), + page, + per_page: perPage + }; + return await getScriptEpisodeListNew(params); + }; + // 修改获取剧集列表函数 const getEpisodeList = async (userId: number, page: number = 1, loadMore: boolean = false) => { // 检查是否正在加载该页 @@ -83,13 +134,15 @@ export default function CreateToVideo2() { } try { - const params = { - user_id: String(userId), - page, - per_page: perPage - }; - - const episodeListResponse = await getScriptEpisodeListNew(params); + let episodeListResponse; + + // 如果有预加载的数据且页码匹配,直接使用 + if (preloadedDataRef.current && preloadedDataRef.current.page === page) { + episodeListResponse = preloadedDataRef.current.data; + preloadedDataRef.current = null; + } else { + episodeListResponse = await fetchEpisodeData(userId, page); + } if (episodeListResponse.code === 0) { const { movie_projects, total_pages } = episodeListResponse.data; @@ -109,6 +162,11 @@ export default function CreateToVideo2() { setTotalPages(total_pages); setHasMore(page < total_pages); setCurrentPage(page); + + // 预加载下一页数据 + // if (page < total_pages && !isPreloading) { + // preloadNextPage(userId, page + 1); + // } } } catch (error) { @@ -188,38 +246,72 @@ export default function CreateToVideo2() { } }; - const renderProjectCard = (project: any) => { + // 监听窗口大小变化,触发 Masonry 重排 + useEffect(() => { + const handleResize = debounce(() => { + if (masonryRef.current?.recomputeCellPositions) { + masonryRef.current.recomputeCellPositions(); + } + }, 200); + + window.addEventListener('resize', handleResize); + return () => { + handleResize.cancel(); + window.removeEventListener('resize', handleResize); + }; + }, []); + + const renderProjectCard = (project: MovieProject): JSX.Element => { + // 根据 aspect_ratio 计算纵横比 + const getAspectRatio = () => { + switch (project.aspect_ratio) { + case "VIDEO_ASPECT_RATIO_LANDSCAPE": + return 16 / 9; // 横屏 16:9 + case "VIDEO_ASPECT_RATIO_PORTRAIT": + return 9 / 16; // 竖屏 9:16 + default: + return 16 / 9; // 默认横屏 + } + }; + + const aspectRatio = getAspectRatio(); return ( -
handleMouseEnter(project.project_id)} onMouseLeave={() => handleMouseLeave(project.project_id)} data-alt="project-card" > - {/* 视频/图片区域 */} -
router.push(`/movies/work-flow?episodeId=${project.project_id}`)}> + {/* 视频/图片区域(使用 aspect_ratio 预设高度) */} +
router.push(`/movies/work-flow?episodeId=${project.project_id}`)} + > {(project.final_video_url || project.final_simple_video_url || project.video_urls) ? (