兼容 竖屏

This commit is contained in:
北枳 2025-09-22 21:18:04 +08:00
parent 93a7b2e68a
commit 6048dd7610
10 changed files with 70 additions and 90 deletions

View File

@ -4,7 +4,7 @@ NEXT_PUBLIC_BASE_URL = https://77.smartvideo.py.qikongjian.com
NEXT_PUBLIC_CUT_URL = https://77.smartcut.py.qikongjian.com NEXT_PUBLIC_CUT_URL = https://77.smartcut.py.qikongjian.com
# 失败率 # 失败率
NEXT_PUBLIC_ERROR_CONFIG = 0.1 NEXT_PUBLIC_ERROR_CONFIG = 0.5
# Google OAuth配置 # Google OAuth配置
NEXT_PUBLIC_GOOGLE_CLIENT_ID=847079918888-o1nne8d3ij80dn20qurivo987pv07225.apps.googleusercontent.com NEXT_PUBLIC_GOOGLE_CLIENT_ID=847079918888-o1nne8d3ij80dn20qurivo987pv07225.apps.googleusercontent.com
NEXT_PUBLIC_GOOGLE_REDIRECT_URI=https://www.movieflow.net/api/auth/google/callback NEXT_PUBLIC_GOOGLE_REDIRECT_URI=https://www.movieflow.net/api/auth/google/callback

View File

@ -1,6 +1,7 @@
import { post } from './request'; import { post } from './request';
import { ApiResponse } from './common'; import { ApiResponse } from './common';
import { Character } from './video_flow'; import { Character } from './video_flow';
import { AspectRatioValue } from '@/components/ChatInputBox/AspectRatioSelector';
// 创建剧集的数据类型 // 创建剧集的数据类型
export interface CreateScriptEpisodeRequest { export interface CreateScriptEpisodeRequest {
@ -78,6 +79,7 @@ interface MovieProject {
last_message: string; last_message: string;
updated_at: string; updated_at: string;
created_at: string; created_at: string;
aspect_ratio: AspectRatioValue;
} }
interface ListMovieProjectsResponse { interface ListMovieProjectsResponse {

View File

@ -469,15 +469,15 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
</Dropdown> </Dropdown>
{/* 分隔线(移动端隐藏,避免拥挤) */} {/* 分隔线(移动端隐藏,避免拥挤) */}
{/* <div className="hidden sm:block w-px h-4 bg-white/[0.20]"></div> */} <div className="hidden sm:block w-px h-4 bg-white/[0.20]"></div>
{/* 横/竖屏选择 上线暂时不开放 */} {/* 横/竖屏选择 */}
{/* <AspectRatioSelector <AspectRatioSelector
value={configOptions.aspect_ratio} value={configOptions.aspect_ratio}
onChange={(v) => onConfigChange('aspect_ratio', v)} onChange={(v) => onConfigChange('aspect_ratio', v)}
placement="top" placement="top"
className={`${isMobile ? '!px-1' : ''}`} className={`${isMobile ? '!px-1' : ''}`}
/> */} />
</div> </div>
{/* 右侧Action按钮 */} {/* 右侧Action按钮 */}

View File

@ -359,12 +359,12 @@ export const H5PhotoStoryDrawer = ({
<div data-alt="bottom-action-bar" className="sticky bottom-0 left-0 right-0 backdrop-blur border-t border-white/10 px-3 py-2"> <div data-alt="bottom-action-bar" className="sticky bottom-0 left-0 right-0 backdrop-blur border-t border-white/10 px-3 py-2">
<div className="flex items-center justify-end gap-2"> <div className="flex items-center justify-end gap-2">
{/* 横/竖屏选择 上线暂时不开放 */} {/* 横/竖屏选择 */}
{/* <AspectRatioSelector <AspectRatioSelector
value={aspectUI} value={aspectUI}
onChange={setAspectUI} onChange={setAspectUI}
placement="top" placement="top"
/> */} />
{!hasAnalyzed ? ( {!hasAnalyzed ? (
<Tooltip title={activeImageUrl ? "Analyze image content" : "Please upload an image first"} placement="top"> <Tooltip title={activeImageUrl ? "Analyze image content" : "Please upload an image first"} placement="top">
<div> <div>

View File

@ -556,12 +556,12 @@ export const H5TemplateDrawer = ({
/> />
</div> </div>
)} )}
{/* 横/竖屏选择 上线暂时不开放 */} {/* 横/竖屏选择 */}
{/* <AspectRatioSelector <AspectRatioSelector
value={aspectUI} value={aspectUI}
onChange={setAspectUI} onChange={setAspectUI}
placement="top" placement="top"
/> */} />
<ActionButton <ActionButton
isCreating={isTemplateCreating || localLoading > 0} isCreating={isTemplateCreating || localLoading > 0}
handleCreateVideo={handleConfirm} handleCreateVideo={handleConfirm}

View File

@ -321,12 +321,12 @@ export const PcPhotoStoryModal = ({
placeholder="Share your creative ideas about the image and let AI create a movie story for you..." placeholder="Share your creative ideas about the image and let AI create a movie story for you..."
/> />
<div className="absolute bottom-1 right-0 flex gap-2 items-center"> <div className="absolute bottom-1 right-0 flex gap-2 items-center">
{/* 横/竖屏选择 上线暂时不开放 */} {/* 横/竖屏选择 */}
{/* <AspectRatioSelector <AspectRatioSelector
value={aspectUI} value={aspectUI}
onChange={setAspectUI} onChange={setAspectUI}
placement="top" placement="top"
/> */} />
{!hasAnalyzed ? ( {!hasAnalyzed ? (
<Tooltip title={activeImageUrl ? "Analyze image content" : "Please upload an image first"} placement="top"> <Tooltip title={activeImageUrl ? "Analyze image content" : "Please upload an image first"} placement="top">
<ActionButton <ActionButton

View File

@ -707,12 +707,12 @@ export const PcTemplateModal = ({
/> />
</div> </div>
)} )}
{/* 横/竖屏选择 上线暂时不开放 */} {/* 横/竖屏选择 */}
{/* <AspectRatioSelector <AspectRatioSelector
value={aspectUI} value={aspectUI}
onChange={setAspectUI} onChange={setAspectUI}
placement="top" placement="top"
/> */} />
<ActionButton <ActionButton
isCreating={isTemplateCreating || localLoading > 0} isCreating={isTemplateCreating || localLoading > 0}
handleCreateVideo={handleConfirm} handleCreateVideo={handleConfirm}

View File

@ -29,8 +29,6 @@ export default function CreateToVideo2() {
const scrollContainerRef = useRef<HTMLDivElement>(null); const scrollContainerRef = useRef<HTMLDivElement>(null);
const [userId, setUserId] = useState<number>(0); const [userId, setUserId] = useState<number>(0);
const [isLoadingDownloadBtn, setIsLoadingDownloadBtn] = useState(false); const [isLoadingDownloadBtn, setIsLoadingDownloadBtn] = useState(false);
/** 保存每个项目的视频朝向 */
const [orientationMap, setOrientationMap] = useState<Record<string, 'portrait' | 'landscape'>>({});
// 添加一个 ref 来跟踪当前正在加载的页码 // 添加一个 ref 来跟踪当前正在加载的页码
const loadingPageRef = useRef<number | null>(null); const loadingPageRef = useRef<number | null>(null);
@ -38,14 +36,46 @@ export default function CreateToVideo2() {
// 在客户端挂载后读取localStorage // 在客户端挂载后读取localStorage
// 监听滚动事件,实现无限加载 // 监听滚动事件,实现无限加载
// 修改滚动处理函数,添加节流 // 修改滚动处理函数,添加节流
// 修改获取剧集列表函数(提前定义,供滚动与初始化调用) const handleScroll = useCallback(() => {
const getEpisodeList = useCallback(async (userId: number, page: number = 1, loadMore: boolean = false) => { if (!scrollContainerRef.current || !hasMore || isLoadingMore || isLoading) return;
const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.current;
if (scrollHeight - scrollTop - clientHeight < 100) {
// 直接使用 currentPage不再使用 setCurrentPage 的回调
const nextPage = currentPage + 1;
if (nextPage <= totalPages) {
getEpisodeList(userId, nextPage, true);
}
}
}, [hasMore, isLoadingMore, isLoading, totalPages, userId, currentPage]);
useEffect(() => {
if (typeof window !== 'undefined') {
const currentUser = JSON.parse(localStorage.getItem('currentUser') || '{}');
console.log('currentUser', currentUser);
setUserId(currentUser.id);
getEpisodeList(currentUser.id, 1, false);
}
}, []);
// 添加滚动监听
useEffect(() => {
const scrollContainer = scrollContainerRef.current;
if (scrollContainer) {
scrollContainer.addEventListener('scroll', handleScroll);
return () => scrollContainer.removeEventListener('scroll', handleScroll);
}
}, [handleScroll]);
// 修改获取剧集列表函数
const getEpisodeList = async (userId: number, page: number = 1, loadMore: boolean = false) => {
// 检查是否正在加载该页 // 检查是否正在加载该页
if (loadingPageRef.current === page) return; if (loadingPageRef.current === page) return;
if (isLoading || (isLoadingMore && !loadMore)) return;
// 设置当前正在加载的页码 // 设置当前正在加载的页码
loadingPageRef.current = page; loadingPageRef.current = page;
if (loadMore) { if (loadMore) {
setIsLoadingMore(true); setIsLoadingMore(true);
} else { } else {
@ -63,7 +93,7 @@ export default function CreateToVideo2() {
if (episodeListResponse.code === 0) { if (episodeListResponse.code === 0) {
const { movie_projects, total_pages } = episodeListResponse.data; const { movie_projects, total_pages } = episodeListResponse.data;
// 确保数据不重复 // 确保数据不重复
if (loadMore) { if (loadMore) {
setEpisodeList(prev => { setEpisodeList(prev => {
@ -75,7 +105,7 @@ export default function CreateToVideo2() {
} else { } else {
setEpisodeList(movie_projects); setEpisodeList(movie_projects);
} }
setTotalPages(total_pages); setTotalPages(total_pages);
setHasMore(page < total_pages); setHasMore(page < total_pages);
setCurrentPage(page); setCurrentPage(page);
@ -89,39 +119,7 @@ export default function CreateToVideo2() {
// 清除当前加载页码 // 清除当前加载页码
loadingPageRef.current = null; loadingPageRef.current = null;
} }
}, [perPage]); };
const handleScroll = useCallback(() => {
if (!scrollContainerRef.current || !hasMore || isLoadingMore || isLoading) return;
const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.current;
if (scrollHeight - scrollTop - clientHeight < 100) {
// 直接使用 currentPage不再使用 setCurrentPage 的回调
const nextPage = currentPage + 1;
if (nextPage <= totalPages) {
getEpisodeList(userId, nextPage, true);
}
}
}, [hasMore, isLoadingMore, isLoading, totalPages, userId, currentPage, getEpisodeList]);
useEffect(() => {
if (typeof window !== 'undefined') {
const currentUser = JSON.parse(localStorage.getItem('currentUser') || '{}');
console.log('currentUser', currentUser);
setUserId(currentUser.id);
getEpisodeList(currentUser.id, 1, false);
}
}, [getEpisodeList]);
// 添加滚动监听
useEffect(() => {
const scrollContainer = scrollContainerRef.current;
if (scrollContainer) {
scrollContainer.addEventListener('scroll', handleScroll);
return () => scrollContainer.removeEventListener('scroll', handleScroll);
}
}, [handleScroll]);
// 原函数位置已上移
// 视频上传和创建功能已迁移到ChatInputBox组件中 // 视频上传和创建功能已迁移到ChatInputBox组件中
@ -190,55 +188,35 @@ export default function CreateToVideo2() {
} }
}; };
/**
* /
* @param {string} projectId -
* @param {HTMLVideoElement} videoEl -
*/
const handleVideoMetadata = (projectId: string, videoEl: HTMLVideoElement) => {
const { videoWidth, videoHeight } = videoEl;
if (!videoWidth || !videoHeight) return;
setOrientationMap((prev) => {
if (prev[projectId]) return prev;
const next = videoWidth >= videoHeight ? 'landscape' : 'portrait';
return { ...prev, [projectId]: next };
});
};
const renderProjectCard = (project: any) => { const renderProjectCard = (project: any) => {
return ( return (
<LazyLoad once> <LazyLoad once>
<div <div
key={project.project_id} key={project.project_id}
className="group flex flex-col bg-black/20 rounded-lg overflow-hidden cursor-pointer hover:bg-white/5 transition-all duration-300 mb-6 break-inside-avoid" className="group flex flex-col bg-black/20 rounded-lg overflow-hidden cursor-pointer hover:bg-white/5 transition-all duration-300"
onMouseEnter={() => handleMouseEnter(project.project_id)} onMouseEnter={() => handleMouseEnter(project.project_id)}
onMouseLeave={() => handleMouseLeave(project.project_id)} onMouseLeave={() => handleMouseLeave(project.project_id)}
data-alt="project-card" data-alt="project-card"
> >
{/* 视频/图片区域 */} {/* 视频/图片区域 */}
<div <div className="relative w-full pb-[56.25%]" onClick={() => router.push(`/movies/work-flow?episodeId=${project.project_id}`)}>
className={`relative ${orientationMap[project.project_id] === 'portrait' ? 'aspect-[9/16]' : 'aspect-video'}`}
onClick={() => router.push(`/movies/work-flow?episodeId=${project.project_id}`)}
data-alt="media-container"
>
{(project.final_video_url || project.final_simple_video_url || project.video_urls) ? ( {(project.final_video_url || project.final_simple_video_url || project.video_urls) ? (
<video <video
ref={(el) => setVideoRef(project.project_id, el)} ref={(el) => setVideoRef(project.project_id, el)}
src={project.final_video_url || project.final_simple_video_url || project.video_urls} src={project.final_video_url || project.final_simple_video_url || project.video_urls}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" className="absolute inset-0 w-full h-full object-contain group-hover:scale-105 transition-transform duration-500"
muted muted
loop loop
playsInline playsInline
preload="metadata" preload="none"
onLoadedMetadata={(e) => handleVideoMetadata(project.project_id, e.currentTarget)}
poster={ poster={
getFirstFrame(project.final_video_url || project.final_simple_video_url || project.video_urls) getFirstFrame(project.final_video_url || project.final_simple_video_url || project.video_urls)
} }
/> />
) : ( ) : (
<div <div
className="w-full h-full bg-cover bg-center group-hover:scale-105 transition-transform duration-500" className="absolute inset-0 w-full h-full bg-contain bg-center bg-no-repeat group-hover:scale-105 transition-transform duration-500"
style={{ style={{
backgroundImage: `url(${cover_image1.src})`, backgroundImage: `url(${cover_image1.src})`,
}} }}
@ -281,7 +259,7 @@ export default function CreateToVideo2() {
</Tooltip> */} </Tooltip> */}
</div> </div>
{/* TODO 删除 */} {/* TODO 删除 */}
{/* <Tooltip title="Delete"></Tooltip> {/* <Tooltip title="Delete">
<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" /></Button> <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" /></Button>
</Tooltip> */} </Tooltip> */}
</div> </div>
@ -304,7 +282,7 @@ export default function CreateToVideo2() {
{episodeList.length > 0 && ( {episodeList.length > 0 && (
/* 优化的剧集网格 */ /* 优化的剧集网格 */
<div className="pb-8"> <div className="pb-8">
<div className="columns-1 sm:columns-2 lg:columns-3" style={{ columnGap: '1.5rem' }} data-alt="masonry-grid"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{episodeList.map(renderProjectCard)} {episodeList.map(renderProjectCard)}
</div> </div>

View File

@ -624,7 +624,7 @@ Please process this video editing request.`;
</div> </div>
{/* 智能对话弹窗 */} {/* 智能对话弹窗 */}
{/* <Drawer <Drawer
width="25%" width="25%"
placement="right" placement="right"
closable={false} closable={false}
@ -664,7 +664,7 @@ Please process this video editing request.`;
}} }}
aiEditingResult={aiEditingResult} aiEditingResult={aiEditingResult}
/> />
</Drawer> */} </Drawer>
<EditModal <EditModal
isOpen={isEditModalOpen} isOpen={isEditModalOpen}

View File

@ -191,7 +191,7 @@ export function ThumbnailGrid({
key={`video-${urls}-${index}`} key={`video-${urls}-${index}`}
className={`relative aspect-auto rounded-lg overflow-hidden className={`relative aspect-auto rounded-lg overflow-hidden
${(currentSketchIndex === index && !disabled && selectedView !== 'final') ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'} ${(currentSketchIndex === index && !disabled && selectedView !== 'final') ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}
${aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? 'min-w-[200px]' : 'min-w-[70px]'} ${aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? 'min-w-[210px]' : 'min-w-[70px]'}
`} `}
onClick={() => !isDragging && !disabled && onSketchSelect(index)} onClick={() => !isDragging && !disabled && onSketchSelect(index)}
> >
@ -276,7 +276,7 @@ export function ThumbnailGrid({
key={sketch.uniqueId || `sketch-${sketch.url}-${index}`} key={sketch.uniqueId || `sketch-${sketch.url}-${index}`}
className={`relative aspect-auto rounded-lg overflow-hidden className={`relative aspect-auto rounded-lg overflow-hidden
${currentSketchIndex === index ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'} ${currentSketchIndex === index ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}
${aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? 'min-w-[200px]' : 'min-w-[70px]'} ${aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? 'min-w-[210px]' : 'min-w-[70px]'}
`} `}
onClick={() => !isDragging && onSketchSelect(index)} onClick={() => !isDragging && onSketchSelect(index)}
> >