forked from 77media/video-flow
调用接口
This commit is contained in:
parent
840a1c093b
commit
0269ec3038
1
api/constants.ts
Normal file
1
api/constants.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const BASE_URL = "https://movieflow.api.huiying.video"
|
||||||
65
api/enums.ts
Normal file
65
api/enums.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
// 主项目(产品)类型枚举
|
||||||
|
export enum ProjectTypeEnum {
|
||||||
|
SCRIPT_TO_VIDEO = 1, // "剧本转视频"
|
||||||
|
VIDEO_TO_VIDEO = 2, // "视频复刻视频"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模式枚举
|
||||||
|
export enum ModeEnum {
|
||||||
|
MANUAL = 1, // "手动"
|
||||||
|
AUTOMATIC = 2, // "自动"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分辨率枚举
|
||||||
|
export enum ResolutionEnum {
|
||||||
|
HD_720P = 1, // "720p"
|
||||||
|
FULL_HD_1080P = 2, // "1080p"
|
||||||
|
UHD_2K = 3, // "2k"
|
||||||
|
UHD_4K = 4, // "4k"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 项目类型映射
|
||||||
|
export const ProjectTypeMap = {
|
||||||
|
[ProjectTypeEnum.SCRIPT_TO_VIDEO]: {
|
||||||
|
value: "script_to_video",
|
||||||
|
label: "剧本转视频",
|
||||||
|
tab: "script"
|
||||||
|
},
|
||||||
|
[ProjectTypeEnum.VIDEO_TO_VIDEO]: {
|
||||||
|
value: "video_to_video",
|
||||||
|
label: "视频复刻视频",
|
||||||
|
tab: "clone"
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// 模式映射
|
||||||
|
export const ModeMap = {
|
||||||
|
[ModeEnum.MANUAL]: {
|
||||||
|
value: "manual",
|
||||||
|
label: "手动"
|
||||||
|
},
|
||||||
|
[ModeEnum.AUTOMATIC]: {
|
||||||
|
value: "automatic",
|
||||||
|
label: "自动"
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// 分辨率映射
|
||||||
|
export const ResolutionMap = {
|
||||||
|
[ResolutionEnum.HD_720P]: {
|
||||||
|
value: "720p",
|
||||||
|
label: "720P"
|
||||||
|
},
|
||||||
|
[ResolutionEnum.FULL_HD_1080P]: {
|
||||||
|
value: "1080p",
|
||||||
|
label: "1080P"
|
||||||
|
},
|
||||||
|
[ResolutionEnum.UHD_2K]: {
|
||||||
|
value: "2k",
|
||||||
|
label: "2K"
|
||||||
|
},
|
||||||
|
[ResolutionEnum.UHD_4K]: {
|
||||||
|
value: "4k",
|
||||||
|
label: "4K"
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig, AxiosHeaders } from 'axios';
|
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig, AxiosHeaders } from 'axios';
|
||||||
|
import { BASE_URL } from './constants'
|
||||||
// 创建 axios 实例
|
// 创建 axios 实例
|
||||||
const request: AxiosInstance = axios.create({
|
const request: AxiosInstance = axios.create({
|
||||||
baseURL: 'https://77.smartvideo.py.qikongjian.com', // 设置基础URL
|
baseURL: BASE_URL, // 设置基础URL
|
||||||
timeout: 10000, // 请求超时时间
|
timeout: 10000, // 请求超时时间
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
47
api/script_episode.ts
Normal file
47
api/script_episode.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { post } from './request';
|
||||||
|
|
||||||
|
// 创建剧集的数据类型
|
||||||
|
export interface CreateScriptEpisodeRequest {
|
||||||
|
title?: string;
|
||||||
|
script_id?: number;
|
||||||
|
characters?: {
|
||||||
|
characters?: Array<{
|
||||||
|
name?: string;
|
||||||
|
desc?: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
summary?: string;
|
||||||
|
atmosphere?: string;
|
||||||
|
scene?: string;
|
||||||
|
status?: number;
|
||||||
|
task_description?: string;
|
||||||
|
creator_name?: string;
|
||||||
|
cate_tags?: {
|
||||||
|
tags?: string[];
|
||||||
|
};
|
||||||
|
episode_sort?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建剧集响应数据类型
|
||||||
|
export interface ScriptEpisode {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
script_id: number;
|
||||||
|
status: number;
|
||||||
|
episode_sort: number;
|
||||||
|
creator_name: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建剧集响应格式
|
||||||
|
export interface EpisodeApiResponse<T> {
|
||||||
|
code: number;
|
||||||
|
msg: string;
|
||||||
|
data: T;
|
||||||
|
successfull: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建剧集
|
||||||
|
export const createScriptEpisode = async (data: CreateScriptEpisodeRequest): Promise<EpisodeApiResponse<ScriptEpisode>> => {
|
||||||
|
return post<EpisodeApiResponse<ScriptEpisode>>('/script_episode/create', data);
|
||||||
|
};
|
||||||
48
api/script_project.ts
Normal file
48
api/script_project.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { post } from './request';
|
||||||
|
|
||||||
|
// 创建剧本项目的数据类型
|
||||||
|
export interface CreateScriptProjectRequest {
|
||||||
|
title?: string;
|
||||||
|
script_author?: string;
|
||||||
|
characters?: Array<{
|
||||||
|
name?: string;
|
||||||
|
desc?: string;
|
||||||
|
}>;
|
||||||
|
summary?: string;
|
||||||
|
project_type?: number;
|
||||||
|
status?: number;
|
||||||
|
cate_tags?: string[];
|
||||||
|
creator_name?: string;
|
||||||
|
mode?: number;
|
||||||
|
resolution?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API响应类型
|
||||||
|
export interface ApiResponse<T> {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建剧本项目响应数据类型
|
||||||
|
export interface ScriptProject {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
script_author: string;
|
||||||
|
characters: Array<{
|
||||||
|
name: string;
|
||||||
|
desc: string;
|
||||||
|
}>;
|
||||||
|
summary: string;
|
||||||
|
project_type: number;
|
||||||
|
status: number;
|
||||||
|
cate_tags: string[];
|
||||||
|
creator_name: string;
|
||||||
|
mode: number;
|
||||||
|
resolution: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建剧本项目
|
||||||
|
export const createScriptProject = async (data: CreateScriptProjectRequest): Promise<ApiResponse<ScriptProject>> => {
|
||||||
|
return post<ApiResponse<ScriptProject>>('/script_project/create', data);
|
||||||
|
};
|
||||||
@ -1,5 +1,5 @@
|
|||||||
// import { redirect } from 'next/navigation';
|
// import { redirect } from 'next/navigation';
|
||||||
import { CreateToVideo2 } from '@/components/pages/create-to-video2';
|
import { CreateToVideo2 } from '@/components/pages/create-to-video';
|
||||||
|
|
||||||
export default function CreatePage() {
|
export default function CreatePage() {
|
||||||
// redirect('/create/video-to-video');
|
// redirect('/create/video-to-video');
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { DashboardLayout } from '@/components/layout/dashboard-layout';
|
import { DashboardLayout } from '@/components/layout/dashboard-layout';
|
||||||
// import { HomePage } from '@/components/pages/home-page';
|
// import { HomePage } from '@/components/pages/home-page';
|
||||||
import { HomePage2 } from '@/components/pages/home-page2';
|
import { HomePage2 } from '@/components/pages/homepage';
|
||||||
import OAuthCallbackHandler from '@/components/ui/oauth-callback-handler';
|
import OAuthCallbackHandler from '@/components/ui/oauth-callback-handler';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,482 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState, useEffect, useRef } from 'react';
|
|
||||||
import { Card } from '@/components/ui/card';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { ArrowLeft, ChevronDown, ChevronUp, Video, ListOrdered, Play, Loader2, Pause, MoreHorizontal, Edit2, Check, X, RefreshCw, Lightbulb, Package, Crown, ArrowUp } from 'lucide-react';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger, SheetClose } from "@/components/ui/sheet";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import './style/create-to-video2.css';
|
|
||||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
|
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
|
||||||
import LiquidGlass from '@/plugins/liquid-glass/index'
|
|
||||||
import { Dropdown } from 'antd';
|
|
||||||
import type { MenuProps } from 'antd';
|
|
||||||
import Image from 'next/image';
|
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
|
|
||||||
const JoyrideNoSSR = dynamic(() => import('react-joyride'), {
|
|
||||||
ssr: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加自定义滚动条样式
|
|
||||||
const scrollbarStyles = `
|
|
||||||
.custom-scrollbar::-webkit-scrollbar {
|
|
||||||
width: 4px;
|
|
||||||
}
|
|
||||||
.custom-scrollbar::-webkit-scrollbar-track {
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface SceneVideo {
|
|
||||||
id: number;
|
|
||||||
video_url: string;
|
|
||||||
script: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ideaText = 'a cute capybara with an orange on its head, staring into the distance and walking forward';
|
|
||||||
|
|
||||||
export function CreateToVideo2() {
|
|
||||||
const router = useRouter();
|
|
||||||
const [isClient, setIsClient] = useState(false);
|
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
|
||||||
const [videoUrl, setVideoUrl] = useState('');
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [activeTab, setActiveTab] = useState('script');
|
|
||||||
const [isFocus, setIsFocus] = useState(false);
|
|
||||||
const [selectedMode, setSelectedMode] = useState('auto');
|
|
||||||
const [selectedResolution, setSelectedResolution] = useState('720P');
|
|
||||||
const [inputText, setInputText] = useState('');
|
|
||||||
const editorRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [runTour, setRunTour] = useState(true);
|
|
||||||
|
|
||||||
const handleUploadVideo = () => {
|
|
||||||
console.log('upload video');
|
|
||||||
// 打开文件选择器
|
|
||||||
const input = document.createElement('input');
|
|
||||||
input.type = 'file';
|
|
||||||
input.accept = 'video/*';
|
|
||||||
input.onchange = (e) => {
|
|
||||||
const file = (e.target as HTMLInputElement).files?.[0];
|
|
||||||
if (file) {
|
|
||||||
setVideoUrl(URL.createObjectURL(file));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
input.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCreateVideo = async () => {
|
|
||||||
if (videoUrl || inputText) {
|
|
||||||
if (activeTab === 'script') {
|
|
||||||
router.push('/create/work-flow');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 下拉菜单项配置
|
|
||||||
const modeItems: MenuProps['items'] = [
|
|
||||||
{
|
|
||||||
type: 'group',
|
|
||||||
label: (
|
|
||||||
<div className="text-white/50 text-xs px-2 pb-2">Mode</div>
|
|
||||||
),
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
key: 'auto',
|
|
||||||
label: (
|
|
||||||
<div className="flex flex-col gap-1 p-1">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-base font-medium">Auto</span>
|
|
||||||
<Crown className="w-4 h-4 text-yellow-500" />
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-gray-400">Automatically selects the best model for optimal efficiency</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'stable',
|
|
||||||
label: (
|
|
||||||
<div className="flex flex-col gap-1 p-1">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-base font-medium">Stable</span>
|
|
||||||
<Crown className="w-4 h-4 text-yellow-500" />
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-gray-400">Offers reliable, consistent performance every time</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'inspire',
|
|
||||||
label: (
|
|
||||||
<div className="flex flex-col gap-1 p-1">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-base font-medium">Inspire</span>
|
|
||||||
<Crown className="w-4 h-4 text-yellow-500" />
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-gray-400">Unleash your creativity with diverse outputs</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// 分辨率选项配置
|
|
||||||
const resolutionItems: MenuProps['items'] = [
|
|
||||||
{
|
|
||||||
type: 'group',
|
|
||||||
label: (
|
|
||||||
<div className="text-white/50 text-xs px-2 pb-2">Resolution</div>
|
|
||||||
),
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
key: '720P',
|
|
||||||
label: (
|
|
||||||
<div className="flex items-center justify-between p-1">
|
|
||||||
<span className="text-base">720P</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: '1080P',
|
|
||||||
label: (
|
|
||||||
<div className="flex items-center justify-between p-1">
|
|
||||||
<span className="text-base">1080P</span>
|
|
||||||
<Crown className="w-4 h-4 text-yellow-500" />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: '2K',
|
|
||||||
label: (
|
|
||||||
<div className="flex items-center justify-between p-1">
|
|
||||||
<span className="text-base">2K</span>
|
|
||||||
<Crown className="w-4 h-4 text-yellow-500" />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: '4K',
|
|
||||||
label: (
|
|
||||||
<div className="flex items-center justify-between p-1">
|
|
||||||
<span className="text-base">4K</span>
|
|
||||||
<Crown className="w-4 h-4 text-yellow-500" />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// 处理模式选择
|
|
||||||
const handleModeSelect: MenuProps['onClick'] = ({ key }) => {
|
|
||||||
setSelectedMode(key);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理分辨率选择
|
|
||||||
const handleResolutionSelect: MenuProps['onClick'] = ({ key }) => {
|
|
||||||
setSelectedResolution(key as string);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleStartCreating = () => {
|
|
||||||
setActiveTab('script');
|
|
||||||
setInputText(ideaText);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理编辑器聚焦
|
|
||||||
const handleEditorFocus = () => {
|
|
||||||
setIsFocus(true);
|
|
||||||
if (editorRef.current && inputText) {
|
|
||||||
// 创建范围对象
|
|
||||||
const range = document.createRange();
|
|
||||||
const selection = window.getSelection();
|
|
||||||
|
|
||||||
// 获取编辑器内的文本节点
|
|
||||||
const textNode = Array.from(editorRef.current.childNodes).find(
|
|
||||||
node => node.nodeType === Node.TEXT_NODE
|
|
||||||
) || editorRef.current.appendChild(document.createTextNode(inputText));
|
|
||||||
|
|
||||||
// 设置范围到文本末尾
|
|
||||||
range.setStart(textNode, inputText.length);
|
|
||||||
range.setEnd(textNode, inputText.length);
|
|
||||||
|
|
||||||
// 应用选择
|
|
||||||
selection?.removeAllRanges();
|
|
||||||
selection?.addRange(range);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理编辑器内容变化
|
|
||||||
const handleEditorChange = (e: React.FormEvent<HTMLDivElement>) => {
|
|
||||||
const newText = e.currentTarget.textContent || '';
|
|
||||||
setInputText(newText);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 引导步骤
|
|
||||||
const steps: Step[] = [
|
|
||||||
{
|
|
||||||
target: '.video-storyboard-tools',
|
|
||||||
content: 'Welcome to AI Video Creation Tool! This is the main creation area.',
|
|
||||||
placement: 'top',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
target: '.storyboard-tools-tab',
|
|
||||||
content: 'Choose between Script mode to create videos from text, or Clone mode to recreate existing videos.',
|
|
||||||
placement: 'bottom',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
target: '.video-prompt-editor',
|
|
||||||
content: 'Describe your video content here. Our AI will generate videos based on your description.',
|
|
||||||
placement: 'top',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
target: '.tool-operation-button',
|
|
||||||
content: 'Select different creation modes and video resolutions to customize your output.',
|
|
||||||
placement: 'top',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
target: '.tool-submit-button',
|
|
||||||
content: 'Click here to start creating your video once you are ready!',
|
|
||||||
placement: 'left',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// 处理引导结束
|
|
||||||
const handleJoyrideCallback = (data: any) => {
|
|
||||||
const { status } = data;
|
|
||||||
if (status === 'finished' || status === 'skipped') {
|
|
||||||
setRunTour(false);
|
|
||||||
// 可以在这里存储用户已完成引导的状态
|
|
||||||
localStorage.setItem('hasCompletedTour', 'true');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 检查是否需要显示引导
|
|
||||||
useEffect(() => {
|
|
||||||
const hasCompletedTour = localStorage.getItem('hasCompletedTour');
|
|
||||||
if (hasCompletedTour) {
|
|
||||||
setRunTour(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsClient(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={containerRef}
|
|
||||||
className="container mx-auto overflow-hidden custom-scrollbar"
|
|
||||||
style={isExpanded ? {height: 'calc(100vh - 12rem)'} : {height: 'calc(100vh - 20rem)'}}
|
|
||||||
>
|
|
||||||
{isClient && (
|
|
||||||
<JoyrideNoSSR
|
|
||||||
steps={steps}
|
|
||||||
run={runTour}
|
|
||||||
continuous
|
|
||||||
showSkipButton
|
|
||||||
showProgress
|
|
||||||
styles={{
|
|
||||||
options: {
|
|
||||||
primaryColor: '#4F46E5',
|
|
||||||
zIndex: 10000,
|
|
||||||
backgroundColor: '#1F2937',
|
|
||||||
overlayColor: 'rgba(0, 0, 0, 0.5)',
|
|
||||||
textColor: '#F3F4F6',
|
|
||||||
arrowColor: '#1F2937',
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
borderRadius: '1rem',
|
|
||||||
},
|
|
||||||
tooltipContainer: {
|
|
||||||
textAlign: 'left',
|
|
||||||
},
|
|
||||||
buttonNext: {
|
|
||||||
fontSize: '14px',
|
|
||||||
},
|
|
||||||
buttonBack: {
|
|
||||||
fontSize: '14px',
|
|
||||||
},
|
|
||||||
buttonSkip: {
|
|
||||||
fontSize: '14px',
|
|
||||||
},
|
|
||||||
buttonClose: {
|
|
||||||
fontSize: '14px',
|
|
||||||
},
|
|
||||||
spotlight: {
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
disableOverlayClose
|
|
||||||
spotlightClicks
|
|
||||||
hideCloseButton
|
|
||||||
callback={handleJoyrideCallback}
|
|
||||||
locale={{
|
|
||||||
back: 'Back',
|
|
||||||
close: 'Close',
|
|
||||||
last: 'Finish',
|
|
||||||
next: 'Next',
|
|
||||||
skip: 'Skip'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className='scroll-load-box h-full overflow-y-scroll w-[calc(100%-16px)] mx-auto'>
|
|
||||||
<div className='min-h-[100%]'>
|
|
||||||
<div className='flex flex-col items-center fixed top-1/2 left-1/2 -translate-x-1/2 translate-y-[calc(-50%-68px)]'>
|
|
||||||
<Image
|
|
||||||
src='/assets/empty_video.png'
|
|
||||||
width={160}
|
|
||||||
height={160}
|
|
||||||
alt='empty_video'
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<div className='text-[16px] font-[400] leading-[24px]'>
|
|
||||||
<span className='opacity-60'>Generated videos will appear here. </span>
|
|
||||||
<span className='font-[700] border-0 border-solid border-white hover:border-b-[1px] cursor-pointer' onClick={() => handleStartCreating()}>Start creating!</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 工具栏 */}
|
|
||||||
<div className='video-tool-component relative w-[1080px]'>
|
|
||||||
<div className='video-storyboard-tools grid gap-4 rounded-[20px] bg-[#0C0E11] backdrop-blur-[15px]'>
|
|
||||||
{isExpanded ? (
|
|
||||||
<div className='absolute top-0 bottom-0 left-0 right-0 z-[1] grid justify-items-center place-content-center rounded-[20px] bg-[#191B1E] bg-opacity-[0.3] backdrop-blur-[1.5px] cursor-pointer' onClick={() => setIsExpanded(false)}>
|
|
||||||
{/* 图标 展开按钮 */}
|
|
||||||
<ChevronUp className='w-4 h-4' />
|
|
||||||
<span className='text-sm'>Click to create</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className='absolute top-[-8px] left-[50%] z-[2] flex items-center justify-center w-[46px] h-4 rounded-[15px] bg-[#fff3] translate-x-[-50%] cursor-pointer hover:bg-[#ffffff1a]' onClick={() => setIsExpanded(true)}>
|
|
||||||
{/* 图标 折叠按钮 */}
|
|
||||||
<ChevronDown className='w-4 h-4' />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className='storyboard-tools-tab relative flex gap-8 px-4 py-[10px]'>
|
|
||||||
<div className={`tab-item ${activeTab === 'script' ? 'active' : ''} cursor-pointer`} onClick={() => setActiveTab('script')}>
|
|
||||||
<span className='text-lg opacity-60'>script</span>
|
|
||||||
</div>
|
|
||||||
<div className={`tab-item ${activeTab === 'clone' ? 'active' : ''} cursor-pointer`} onClick={() => setActiveTab('clone')}>
|
|
||||||
<span className='text-lg opacity-60'>clone</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`flex-shrink-0 p-4 overflow-hidden transition-all duration-300 pt-0 gap-4 ${isExpanded ? 'h-[16px]' : 'h-[162px]'}`}>
|
|
||||||
<div className='video-creation-tool-container flex flex-col gap-4'>
|
|
||||||
{activeTab === 'clone' && (
|
|
||||||
<div className='relative flex items-center gap-4 h-[94px]'>
|
|
||||||
<div className='relative w-[72px] rounded-[6px] overflow-hidden cursor-pointer ant-dropdown-trigger' onClick={handleUploadVideo}>
|
|
||||||
<div className='relative flex items-center justify-center w-[72px] h-[72px] rounded-[6px 6px 0 0] bg-white/[0.05] transition-all overflow-hidden hover:bg-white/[0.10]'>
|
|
||||||
{/* 图标 添加视频 */}
|
|
||||||
<Video className='w-4 h-4' />
|
|
||||||
</div>
|
|
||||||
<div className='w-full h-[22px] flex items-center justify-center rounded-[0 0 6px 6px] bg-white/[0.03]'>
|
|
||||||
<span className='text-xs opacity-30 cursor-[inherit]'>Add Video</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{videoUrl && (
|
|
||||||
<div className='relative w-[72px] rounded-[6px] overflow-hidden cursor-pointer ant-dropdown-trigger'>
|
|
||||||
<div className='relative flex items-center justify-center w-[72px] h-[72px] rounded-[6px 6px 0 0] bg-white/[0.05] transition-all overflow-hidden hover:bg-white/[0.10]'>
|
|
||||||
<video src={videoUrl} className='w-full h-full object-cover' />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{activeTab === 'script' && (
|
|
||||||
<div className='relative flex items-center gap-4 h-[94px]'>
|
|
||||||
<div className={`video-prompt-editor relative flex flex-1 self-stretch items-center w-0 rounded-[6px] ${isFocus ? 'focus' : ''}`}>
|
|
||||||
<div
|
|
||||||
ref={editorRef}
|
|
||||||
className='editor-content flex-1 w-0 max-h-[78px] min-h-[26px] h-auto gap-4 pl-[10px] rounded-[10px] leading-[26px] text-sm border-none overflow-y-auto cursor-text'
|
|
||||||
contentEditable
|
|
||||||
style={{ paddingRight: '10px' }}
|
|
||||||
onFocus={handleEditorFocus}
|
|
||||||
onBlur={() => setIsFocus(false)}
|
|
||||||
onInput={handleEditorChange}
|
|
||||||
suppressContentEditableWarning
|
|
||||||
>
|
|
||||||
{inputText}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={`custom-placeholder absolute top-[50%] left-[10px] z-10 translate-y-[-50%] flex items-center gap-1 pointer-events-none text-[14px] leading-[26px] text-white/[0.40] ${inputText ? 'opacity-0' : 'opacity-100'}`}
|
|
||||||
>
|
|
||||||
<span>Describe the content you want to create. Get an </span>
|
|
||||||
<b
|
|
||||||
className='idea-link inline-flex items-center gap-0.5 text-white/[0.50] font-normal cursor-pointer pointer-events-auto underline'
|
|
||||||
onClick={() => setInputText(ideaText)}
|
|
||||||
>
|
|
||||||
<Lightbulb className='w-4 h-4' />idea
|
|
||||||
</b>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className='flex gap-3'>
|
|
||||||
<div className='tool-scroll-box relative flex-1 w-0'>
|
|
||||||
<div className='tool-scroll-box-content overflow-x-auto scrollbar-hide'>
|
|
||||||
<div className='flex items-center flex-1 gap-3'>
|
|
||||||
<Dropdown
|
|
||||||
menu={{
|
|
||||||
items: modeItems,
|
|
||||||
onClick: handleModeSelect,
|
|
||||||
selectedKeys: [selectedMode],
|
|
||||||
}}
|
|
||||||
trigger={['click']}
|
|
||||||
overlayClassName="mode-dropdown"
|
|
||||||
placement="bottomLeft"
|
|
||||||
>
|
|
||||||
<div className='tool-operation-button ant-dropdown-trigger'>
|
|
||||||
<Package className='w-4 h-4' />
|
|
||||||
<span className='text-nowrap opacity-70'>
|
|
||||||
{selectedMode.charAt(0).toUpperCase() + selectedMode.slice(1)}
|
|
||||||
</span>
|
|
||||||
<Crown className='w-4 h-4 text-yellow-500' />
|
|
||||||
</div>
|
|
||||||
</Dropdown>
|
|
||||||
|
|
||||||
<Dropdown
|
|
||||||
menu={{
|
|
||||||
items: resolutionItems,
|
|
||||||
onClick: handleResolutionSelect,
|
|
||||||
selectedKeys: [selectedResolution],
|
|
||||||
}}
|
|
||||||
trigger={['click']}
|
|
||||||
overlayClassName="mode-dropdown"
|
|
||||||
placement="bottomLeft"
|
|
||||||
>
|
|
||||||
<div className='tool-operation-button ant-dropdown-trigger'>
|
|
||||||
<Video className='w-4 h-4' />
|
|
||||||
<span className='text-nowrap opacity-70'>{selectedResolution}</span>
|
|
||||||
<Crown className='w-4 h-4 text-yellow-500' />
|
|
||||||
</div>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='flex items-center gap-3'>
|
|
||||||
<div className={`tool-submit-button ${videoUrl || inputText ? '' : 'disabled'}`} onClick={handleCreateVideo}>
|
|
||||||
<ArrowUp className='w-4 h-4' />Create
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,163 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Separator } from '@/components/ui/separator';
|
|
||||||
import {
|
|
||||||
Play,
|
|
||||||
Clock,
|
|
||||||
Eye,
|
|
||||||
MoreHorizontal,
|
|
||||||
Plus,
|
|
||||||
Sparkles,
|
|
||||||
} from 'lucide-react';
|
|
||||||
|
|
||||||
const mockTasks = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: 'Tech Product Demo',
|
|
||||||
status: 'completed',
|
|
||||||
duration: '2:45',
|
|
||||||
views: 1234,
|
|
||||||
createdAt: '2024-01-15',
|
|
||||||
thumbnail: 'https://images.pexels.com/photos/3861969/pexels-photo-3861969.jpeg?auto=compress&cs=tinysrgb&w=400',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: 'Marketing Campaign Video',
|
|
||||||
status: 'processing',
|
|
||||||
duration: '1:30',
|
|
||||||
views: 0,
|
|
||||||
createdAt: '2024-01-14',
|
|
||||||
thumbnail: 'https://images.pexels.com/photos/3184287/pexels-photo-3184287.jpeg?auto=compress&cs=tinysrgb&w=400',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: 'Educational Content',
|
|
||||||
status: 'completed',
|
|
||||||
duration: '5:20',
|
|
||||||
views: 856,
|
|
||||||
createdAt: '2024-01-12',
|
|
||||||
thumbnail: 'https://images.pexels.com/photos/3184465/pexels-photo-3184465.jpeg?auto=compress&cs=tinysrgb&w=400',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export function HomePage() {
|
|
||||||
return (
|
|
||||||
<div className="space-y-8">
|
|
||||||
{/* Hero Section */}
|
|
||||||
<div className="text-center space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h1 className="text-4xl font-bold tracking-tight">
|
|
||||||
Create Amazing Videos with{' '}
|
|
||||||
<span className="bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
|
||||||
AI Power
|
|
||||||
</span>
|
|
||||||
</h1>
|
|
||||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
|
||||||
Transform your ideas into professional videos using our advanced AI technology.
|
|
||||||
From script to screen in minutes, not hours.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-center pt-4">
|
|
||||||
<Link href="/create">
|
|
||||||
<Button size="lg" className="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700">
|
|
||||||
<Sparkles className="mr-2 h-5 w-5" />
|
|
||||||
Create AI Video
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
{/* Recent Projects */}
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h2 className="text-2xl font-bold">Recent Projects</h2>
|
|
||||||
<Button variant="outline" size="sm">
|
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
|
||||||
New Project
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
||||||
{mockTasks.map((task) => (
|
|
||||||
<Card key={task.id} className="group hover:shadow-lg transition-all duration-200 cursor-pointer">
|
|
||||||
<CardHeader className="p-0">
|
|
||||||
<div className="relative">
|
|
||||||
<img
|
|
||||||
src={task.thumbnail}
|
|
||||||
alt={task.title}
|
|
||||||
className="w-full h-48 object-cover rounded-t-lg"
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-200 rounded-t-lg flex items-center justify-center">
|
|
||||||
<Button size="sm" variant="secondary">
|
|
||||||
<Play className="mr-2 h-4 w-4" />
|
|
||||||
Preview
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Badge
|
|
||||||
variant={task.status === 'completed' ? 'default' : 'secondary'}
|
|
||||||
className="absolute top-2 right-2"
|
|
||||||
>
|
|
||||||
{task.status}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="pt-4">
|
|
||||||
<CardTitle className="text-lg mb-2">{task.title}</CardTitle>
|
|
||||||
<div className="flex items-center space-x-4 text-sm text-muted-foreground">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Clock className="mr-1 h-3 w-3" />
|
|
||||||
{task.duration}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Eye className="mr-1 h-3 w-3" />
|
|
||||||
{task.views}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted-foreground mt-2">
|
|
||||||
Created on {new Date(task.createdAt).toLocaleDateString()}
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="pt-0">
|
|
||||||
<Button variant="ghost" size="sm" className="ml-auto">
|
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{mockTasks.length === 0 && (
|
|
||||||
<Card className="text-center py-12">
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="mx-auto w-16 h-16 rounded-full bg-muted flex items-center justify-center">
|
|
||||||
<Sparkles className="h-8 w-8 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h3 className="text-xl font-semibold">No projects yet</h3>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Get started by creating your first AI-powered video
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Link href="/create">
|
|
||||||
<Button>
|
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
|
||||||
Create Your First Video
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,55 +1,93 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useRef } from "react";
|
import { useState, useRef } from "react";
|
||||||
import { ArrowRight, Plus, Table, AlignHorizontalSpaceAround } from "lucide-react";
|
import { Plus, Table, AlignHorizontalSpaceAround, Loader2 } from "lucide-react";
|
||||||
import "./style/home-page2.css";
|
import "./style/home-page.css";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { VideoScreenLayout } from '@/components/video-screen-layout';
|
import { VideoScreenLayout } from '@/components/video-screen-layout';
|
||||||
import { VideoGridLayout } from '@/components/video-grid-layout';
|
import { VideoGridLayout } from '@/components/video-grid-layout';
|
||||||
import LiquidGlass from '@/plugins/liquid-glass';
|
import LiquidGlass from '@/plugins/liquid-glass';
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
import {
|
||||||
|
createScriptProject,
|
||||||
|
CreateScriptProjectRequest
|
||||||
|
} from '@/api/script_project';
|
||||||
|
import {
|
||||||
|
createScriptEpisode,
|
||||||
|
CreateScriptEpisodeRequest
|
||||||
|
} from '@/api/script_episode';
|
||||||
|
import {
|
||||||
|
ProjectTypeEnum,
|
||||||
|
ModeEnum,
|
||||||
|
ResolutionEnum
|
||||||
|
} from '@/api/enums';
|
||||||
|
|
||||||
export function HomePage2() {
|
export function HomePage2() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [activeTool, setActiveTool] = useState("stretch");
|
const [activeTool, setActiveTool] = useState("stretch");
|
||||||
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
|
const [createdProjectId, setCreatedProjectId] = useState<number | null>(null);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// 示例视频数据
|
// 视频数据
|
||||||
const videos = [
|
const videos: Array<{id: string; url: string; title: string}> = [];
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
url: 'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4',
|
|
||||||
title: '视频标题 1'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
url: 'https://cdn.qikongjian.com/videos/1750389908_37d4fffa-8516-43a3-a423-fc0274f40e8a_text_to_video_0.mp4',
|
|
||||||
title: '视频标题 2'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
url: 'https://cdn.qikongjian.com/videos/1750384661_d8e30b79-828e-48cd-9025-ab62a996717c_text_to_video_0.mp4',
|
|
||||||
title: '视频标题 3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
url: 'https://cdn.qikongjian.com/videos/1750320040_4b47996e-7c70-490e-8433-80c7df990fdd_text_to_video_0.mp4',
|
|
||||||
title: '视频标题 4'
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// 处理编辑视频
|
// 处理编辑视频
|
||||||
const handleEdit = (id: string) => {
|
const handleEdit = (id: string) => {
|
||||||
console.log('Edit video:', id);
|
|
||||||
// TODO: 实现编辑功能
|
// TODO: 实现编辑功能
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理删除视频
|
// 处理删除视频
|
||||||
const handleDelete = (id: string) => {
|
const handleDelete = (id: string) => {
|
||||||
console.log('Delete video:', id);
|
|
||||||
// TODO: 实现删除功能
|
// TODO: 实现删除功能
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 处理创建项目
|
||||||
|
const handleCreateProject = async () => {
|
||||||
|
if (isCreating) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsCreating(true);
|
||||||
|
|
||||||
|
// 使用默认值
|
||||||
|
const projectType = ProjectTypeEnum.SCRIPT_TO_VIDEO;
|
||||||
|
|
||||||
|
// 构建项目数据并调用API
|
||||||
|
const projectData: CreateScriptProjectRequest = {
|
||||||
|
project_type: projectType,
|
||||||
|
mode: ModeEnum.MANUAL,
|
||||||
|
resolution: ResolutionEnum.FULL_HD_1080P
|
||||||
|
};
|
||||||
|
|
||||||
|
const projectResponse = await createScriptProject(projectData);
|
||||||
|
|
||||||
|
if (projectResponse.code === 0 && projectResponse.data.id) {
|
||||||
|
const projectId = projectResponse.data.id;
|
||||||
|
setCreatedProjectId(projectId);
|
||||||
|
|
||||||
|
// 创建剧集数据
|
||||||
|
const episodeData: CreateScriptEpisodeRequest = {
|
||||||
|
};
|
||||||
|
|
||||||
|
// 调用创建剧集API
|
||||||
|
const episodeResponse = await createScriptEpisode(episodeData);
|
||||||
|
|
||||||
|
if (episodeResponse.code === 0) {
|
||||||
|
// 成功创建后跳转到create页面
|
||||||
|
router.push("/create");
|
||||||
|
} else {
|
||||||
|
alert(`创建剧集失败: ${episodeResponse.msg}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert(`创建项目失败: ${projectResponse.message}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert("创建项目时发生错误,请稍后重试");
|
||||||
|
} finally {
|
||||||
|
setIsCreating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[var(--background)]" ref={containerRef}>
|
<div className="min-h-screen bg-[var(--background)]" ref={containerRef}>
|
||||||
<div className="min-h-[100%] flex relative">
|
<div className="min-h-[100%] flex relative">
|
||||||
@ -99,28 +137,31 @@ export function HomePage2() {
|
|||||||
<div className="fixed bottom-[5rem] left-[50%] -translate-x-1/2 z-50">
|
<div className="fixed bottom-[5rem] left-[50%] -translate-x-1/2 z-50">
|
||||||
<motion.div
|
<motion.div
|
||||||
className="relative group"
|
className="relative group"
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={!isCreating ? { scale: 1.05 } : {}}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={!isCreating ? { scale: 0.95 } : {}}
|
||||||
onClick={() => {
|
onClick={handleCreateProject}
|
||||||
console.log("Create Project")
|
|
||||||
router.push("/create")
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
|
|
||||||
{/* 玻璃按钮 */}
|
{/* 玻璃按钮 */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="add-project-btn relative flex items-center gap-3 px-6 py-4 rounded-2xl
|
className={`add-project-btn relative flex items-center gap-3 px-6 py-4 rounded-2xl
|
||||||
bg-white/20 backdrop-blur-md cursor-pointer
|
bg-white/20 backdrop-blur-md cursor-pointer
|
||||||
shadow-[0_8px_32px_rgba(0,0,0,0.2)] group-hover:shadow-[0_8px_32px_rgba(0,0,0,0.4)]
|
shadow-[0_8px_32px_rgba(0,0,0,0.2)] group-hover:shadow-[0_8px_32px_rgba(0,0,0,0.4)]
|
||||||
transition-all duration-300"
|
transition-all duration-300 ${isCreating ? 'opacity-70 cursor-not-allowed' : ''}`}
|
||||||
initial={false}
|
initial={false}
|
||||||
whileHover={{
|
whileHover={!isCreating ? {
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.15)"
|
backgroundColor: "rgba(255, 255, 255, 0.15)"
|
||||||
}}
|
} : {}}
|
||||||
>
|
>
|
||||||
<Plus className="w-6 h-6 text-white" />
|
{isCreating ? (
|
||||||
|
<Loader2 className="w-6 h-6 text-white animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Plus className="w-6 h-6 text-white" />
|
||||||
|
)}
|
||||||
<div className="btn-text text-lg font-medium bg-gradient-to-r text-white
|
<div className="btn-text text-lg font-medium bg-gradient-to-r text-white
|
||||||
bg-clip-text text-transparent">Create</div>
|
bg-clip-text text-transparent">
|
||||||
|
{isCreating ? "Creating..." : "Create"}
|
||||||
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
@ -2,11 +2,15 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
bottom: 1rem;
|
bottom: 1rem;
|
||||||
z-index: 99;
|
|
||||||
--tw-translate-x: calc(-50% + 34.5px);
|
--tw-translate-x: calc(-50% + 34.5px);
|
||||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.video-storyboard-tools {
|
||||||
|
border: 1px solid rgba(255, 255, 255, .2);
|
||||||
|
box-shadow: 0 4px 20px #0009;
|
||||||
|
}
|
||||||
|
|
||||||
.video-storyboard-tools .tool-submit-button {
|
.video-storyboard-tools .tool-submit-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
@ -58,4 +62,71 @@
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
transform: translate(-50%);
|
transform: translate(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-prompt-editor .editor-content {
|
||||||
|
line-height: 26px;
|
||||||
|
outline: none;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-prompt-editor .editor-content[contenteditable] {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-prompt-editor.focus {
|
||||||
|
background-color: #ffffff0d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.underline {
|
||||||
|
text-decoration-line: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-scroll-box-content {
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-operation-button {
|
||||||
|
display: flex;
|
||||||
|
height: 36px;
|
||||||
|
cursor: pointer;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding-left: 16px;
|
||||||
|
padding-right: 16px;
|
||||||
|
font-size: .875rem;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
background-color: #ffffff0d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-operation-button:hover {
|
||||||
|
background-color: #ffffff1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 下拉菜单样式 */
|
||||||
|
.mode-dropdown.ant-dropdown .ant-dropdown-menu {
|
||||||
|
background: rgba(25, 27, 30, 0.95);
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 8px;
|
||||||
|
min-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-dropdown.ant-dropdown .ant-dropdown-menu-item {
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-dropdown.ant-dropdown .ant-dropdown-menu-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-dropdown.ant-dropdown .ant-dropdown-menu-item-selected {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
@ -1,132 +0,0 @@
|
|||||||
.video-tool-component {
|
|
||||||
position: fixed;
|
|
||||||
left: 50%;
|
|
||||||
bottom: 1rem;
|
|
||||||
--tw-translate-x: calc(-50% + 34.5px);
|
|
||||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-storyboard-tools {
|
|
||||||
border: 1px solid rgba(255, 255, 255, .2);
|
|
||||||
box-shadow: 0 4px 20px #0009;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-storyboard-tools .tool-submit-button {
|
|
||||||
display: flex;
|
|
||||||
height: 36px;
|
|
||||||
width: 120px;
|
|
||||||
cursor: pointer;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 2px;
|
|
||||||
border-radius: 10px;
|
|
||||||
font-size: .875rem;
|
|
||||||
line-height: 1.25rem;
|
|
||||||
font-weight: 600;
|
|
||||||
--tw-text-opacity: 1;
|
|
||||||
color: rgb(29 33 41 / var(--tw-text-opacity));
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-storyboard-tools .tool-submit-button.disabled {
|
|
||||||
background-color: #fff;
|
|
||||||
opacity: .3;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.storyboard-tools-tab {
|
|
||||||
border-radius: 16px 16px 0 0;
|
|
||||||
background: #ffffff0d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.storyboard-tools-tab .tab-item {
|
|
||||||
position: relative;
|
|
||||||
cursor: pointer;
|
|
||||||
--tw-text-opacity: 1;
|
|
||||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
|
||||||
line-height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.storyboard-tools-tab .tab-item.active,
|
|
||||||
.storyboard-tools-tab .tab-item.active span {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.storyboard-tools-tab .tab-item.active:after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
bottom: -8px;
|
|
||||||
left: 50%;
|
|
||||||
width: 30px;
|
|
||||||
height: 2px;
|
|
||||||
border-radius: 2px;
|
|
||||||
background-color: #fff;
|
|
||||||
transform: translate(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-prompt-editor .editor-content {
|
|
||||||
line-height: 26px;
|
|
||||||
outline: none;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-prompt-editor .editor-content[contenteditable] {
|
|
||||||
display: inline-block;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-prompt-editor.focus {
|
|
||||||
background-color: #ffffff0d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.underline {
|
|
||||||
text-decoration-line: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-scroll-box-content {
|
|
||||||
scrollbar-width: none;
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-operation-button {
|
|
||||||
display: flex;
|
|
||||||
height: 36px;
|
|
||||||
cursor: pointer;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
border-radius: 20px;
|
|
||||||
padding-left: 16px;
|
|
||||||
padding-right: 16px;
|
|
||||||
font-size: .875rem;
|
|
||||||
line-height: 1.25rem;
|
|
||||||
background-color: #ffffff0d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-operation-button:hover {
|
|
||||||
background-color: #ffffff1a;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 下拉菜单样式 */
|
|
||||||
.mode-dropdown.ant-dropdown .ant-dropdown-menu {
|
|
||||||
background: rgba(25, 27, 30, 0.95);
|
|
||||||
backdrop-filter: blur(15px);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 8px;
|
|
||||||
min-width: 280px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-dropdown.ant-dropdown .ant-dropdown-menu-item {
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 8px;
|
|
||||||
color: rgba(255, 255, 255, 0.9);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-dropdown.ant-dropdown .ant-dropdown-menu-item:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-dropdown.ant-dropdown .ant-dropdown-menu-item-selected {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
@ -41,7 +41,7 @@ const Parallax = () => {
|
|||||||
|
|
||||||
// 设置卡片容器的旋转角度
|
// 设置卡片容器的旋转角度
|
||||||
if (cards) {
|
if (cards) {
|
||||||
cards.style.transform = `rotateX(${yValue}deg) rotateY(${xValue}deg)`;
|
cards.style.transform = `rotateX(${yValue}deg) rotateY(${xValue}deg)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置所有图片的位移
|
// 设置所有图片的位移
|
||||||
@ -59,11 +59,11 @@ const Parallax = () => {
|
|||||||
if (pageX) {
|
if (pageX) {
|
||||||
// Add the passive flag to the event listener to improve scrolling performance
|
// Add the passive flag to the event listener to improve scrolling performance
|
||||||
pageX.addEventListener('mousemove', parallax as EventListener, { passive: true });
|
pageX.addEventListener('mousemove', parallax as EventListener, { passive: true });
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
pageX.removeEventListener('mousemove', parallax as EventListener);
|
pageX.removeEventListener('mousemove', parallax as EventListener);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@ -54,42 +54,42 @@ const VantaHaloBackground = memo(({ onLoaded }: VantaHaloBackgroundProps) => {
|
|||||||
const vantaScript = document.createElement('script')
|
const vantaScript = document.createElement('script')
|
||||||
vantaScript.src = '/lib/vanta.halo.min.js'
|
vantaScript.src = '/lib/vanta.halo.min.js'
|
||||||
document.body.appendChild(vantaScript)
|
document.body.appendChild(vantaScript)
|
||||||
|
|
||||||
vantaScript.onload = () => {
|
vantaScript.onload = () => {
|
||||||
if (canceled || !vantaRef.current || effectInstance.current) return
|
if (canceled || !vantaRef.current || effectInstance.current) return
|
||||||
|
|
||||||
// 使用 requestAnimationFrame 来控制动画帧率
|
// 使用 requestAnimationFrame 来控制动画帧率
|
||||||
const animate = () => {
|
const animate = () => {
|
||||||
if (effectInstance.current) {
|
if (effectInstance.current) {
|
||||||
effectInstance.current.frameRequestId = requestAnimationFrame(animate)
|
effectInstance.current.frameRequestId = requestAnimationFrame(animate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Access VANTA from the window object after script loads
|
// Access VANTA from the window object after script loads
|
||||||
if (window.VANTA && window.VANTA.HALO) {
|
if (window.VANTA && window.VANTA.HALO) {
|
||||||
effectInstance.current = window.VANTA.HALO({
|
effectInstance.current = window.VANTA.HALO({
|
||||||
el: vantaRef.current,
|
el: vantaRef.current,
|
||||||
THREE: window.THREE, // Use THREE from window instead of import
|
THREE: window.THREE, // Use THREE from window instead of import
|
||||||
mouseControls: true,
|
mouseControls: true,
|
||||||
touchControls: true,
|
touchControls: true,
|
||||||
gyroControls: false,
|
gyroControls: false,
|
||||||
scale: 1.0,
|
scale: 1.0,
|
||||||
scaleMobile: 1.0,
|
scaleMobile: 1.0,
|
||||||
amplitudeFactor: 1.5,
|
amplitudeFactor: 1.5,
|
||||||
ringFactor: 1.3,
|
ringFactor: 1.3,
|
||||||
size: 1.2,
|
size: 1.2,
|
||||||
minHeight: 200.00,
|
minHeight: 200.00,
|
||||||
minWidth: 200.00,
|
minWidth: 200.00,
|
||||||
// 优化渲染性能的参数
|
// 优化渲染性能的参数
|
||||||
fps: 30, // 限制帧率
|
fps: 30, // 限制帧率
|
||||||
renderCacheSize: 4, // 缓存大小
|
renderCacheSize: 4, // 缓存大小
|
||||||
})
|
})
|
||||||
|
|
||||||
frameId.current = requestAnimationFrame(animate)
|
frameId.current = requestAnimationFrame(animate)
|
||||||
|
|
||||||
// 通知加载完成
|
// 通知加载完成
|
||||||
if (onLoaded) {
|
if (onLoaded) {
|
||||||
onLoaded();
|
onLoaded();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -116,7 +116,7 @@ function VideoGridLayoutComponent({ videos, onEdit, onDelete }: VideoGridLayoutP
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export as a client-only component to prevent hydration issues
|
// Export as a client-only component to prevent hydration issues
|
||||||
export const VideoGridLayout = dynamic(() => Promise.resolve(VideoGridLayoutComponent), {
|
export const VideoGridLayout = dynamic(() => Promise.resolve(VideoGridLayoutComponent), {
|
||||||
|
|||||||
@ -126,7 +126,7 @@ function VideoScreenLayoutComponent({ videos }: VideoScreenLayoutProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export as a client-only component to prevent hydration issues
|
// Export as a client-only component to prevent hydration issues
|
||||||
export const VideoScreenLayout = dynamic(() => Promise.resolve(VideoScreenLayoutComponent), {
|
export const VideoScreenLayout = dynamic(() => Promise.resolve(VideoScreenLayoutComponent), {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user