调用接口

This commit is contained in:
Xin Wang 2025-07-01 17:47:11 +08:00
parent 0269ec3038
commit 2b4208fb41
10 changed files with 495 additions and 75 deletions

211
api/common.ts Normal file
View File

@ -0,0 +1,211 @@
// Common API 相关接口
import { BASE_URL } from './constants'
export interface ApiResponse<T = any> {
code: number
successful: boolean
msg: string
data: T
}
export interface TokenResponse {
code: number
message: string
data: {
token: string
}
successfull: boolean
}
export interface TokenWithDomainResponse {
code: number
message: string
data: {
token: string
domain: string
}
successfull: boolean
}
export interface QiniuUploadResponse {
hash: string
key: string
url?: string
}
// 获取七牛云上传token
export const getUploadToken = async (timeoutMs: number = 10000): Promise<string> => {
// 添加超时控制
const controller = new AbortController()
const timeoutId = setTimeout(() => {
console.log(`请求超时(${timeoutMs / 1000}秒),正在中断请求...`)
controller.abort()
}, timeoutMs)
try {
const response = await fetch(`${BASE_URL}/common/get-upload-token`, {
method: "GET",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
signal: controller.signal,
mode: "cors",
})
// 请求完成后清除超时
clearTimeout(timeoutId)
if (!response.ok) {
const errorText = await response.text()
console.error("获取token响应错误:", response.status, errorText)
throw new Error(`获取token失败: ${response.status} ${response.statusText}`)
}
const result: TokenResponse = await response.json()
console.log("获取token响应:", result)
if (result.code === 0 && result.successfull && result.data.token) {
console.log("成功获取token")
return result.data.token
} else {
throw new Error(result.message || "获取token失败服务器未返回有效token")
}
} catch (error) {
clearTimeout(timeoutId)
console.error("获取上传token失败:", error)
if (error instanceof Error) {
if (error.name === "AbortError") {
throw new Error("请求超时,请检查网络连接")
} else if (error.message.includes("Failed to fetch")) {
throw new Error("网络连接失败可能是CORS策略限制或服务器不可达")
}
}
throw error
}
}
// 获取七牛云上传token包含domain
export const getUploadTokenWithDomain = async (timeoutMs: number = 10000): Promise<{ token: string, domain: string }> => {
// 添加超时控制
const controller = new AbortController()
const timeoutId = setTimeout(() => {
console.log(`请求超时(${timeoutMs / 1000}秒),正在中断请求...`)
controller.abort()
}, timeoutMs)
try {
const response = await fetch(`${BASE_URL}/common/get-upload-token`, {
method: "GET",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
signal: controller.signal,
mode: "cors",
})
// 请求完成后清除超时
clearTimeout(timeoutId)
if (!response.ok) {
const errorText = await response.text()
console.error("获取token响应错误:", response.status, errorText)
throw new Error(`获取token失败: ${response.status} ${response.statusText}`)
}
const result: TokenWithDomainResponse | TokenResponse = await response.json()
console.log("获取token响应:", result)
if (result.code === 0 && result.successfull && result.data.token) {
console.log("成功获取token")
// Support both old and new API response formats
const domain = 'domain' in result.data ? result.data.domain : 'cdn.qikongjian.com'
return {
token: result.data.token,
domain: domain
}
} else {
throw new Error(result.message || "获取token失败服务器未返回有效token")
}
} catch (error) {
clearTimeout(timeoutId)
console.error("获取上传token失败:", error)
if (error instanceof Error) {
if (error.name === "AbortError") {
throw new Error("请求超时,请检查网络连接")
} else if (error.message.includes("Failed to fetch")) {
throw new Error("网络连接失败可能是CORS策略限制或服务器不可达")
}
}
throw error
}
}
// 生成唯一文件名
export const generateUniqueFileName = (originalName: string): string => {
const timestamp = Date.now()
const randomStr = Math.random().toString(36).substring(2, 8)
const extension = originalName.split(".").pop()
return `videos/${timestamp}_${randomStr}.${extension}`
}
// 七牛云上传
export const uploadToQiniu = async (
file: File,
token: string,
onProgress?: (progress: number) => void
): Promise<string> => {
const uniqueFileName = generateUniqueFileName(file.name)
const formData = new FormData()
formData.append("token", token)
formData.append("key", uniqueFileName)
formData.append("file", file)
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.upload.addEventListener("progress", (event) => {
if (event.lengthComputable && onProgress) {
const progress = Math.round((event.loaded / event.total) * 100)
onProgress(progress)
}
})
xhr.addEventListener("load", () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
const response: QiniuUploadResponse = JSON.parse(xhr.responseText)
const qiniuUrl = `https://cdn.qikongjian.com/${response.key || uniqueFileName}`
console.log("七牛云上传成功:", response)
resolve(qiniuUrl)
} catch (error) {
console.error("解析响应失败:", error, "原始响应:", xhr.responseText)
reject(new Error(`解析上传响应失败: ${xhr.responseText}`))
}
} else {
console.error("七牛云上传失败:", xhr.status, xhr.statusText, xhr.responseText)
reject(new Error(`上传失败: ${xhr.status} ${xhr.statusText}`))
}
})
xhr.addEventListener("error", (e) => {
console.error("上传网络错误:", e)
reject(new Error("网络错误,上传失败"))
})
xhr.addEventListener("abort", () => {
reject(new Error("上传被取消"))
})
xhr.open("POST", "https://up-z2.qiniup.com")
xhr.send(formData)
})
}

View File

@ -1,4 +1,5 @@
import { post } from './request';
import { ApiResponse } from './common';
// 创建剧集的数据类型
export interface CreateScriptEpisodeRequest {
@ -33,15 +34,7 @@ export interface ScriptEpisode {
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);
export const createScriptEpisode = async (data: CreateScriptEpisodeRequest): Promise<ApiResponse<ScriptEpisode>> => {
return post<ApiResponse<ScriptEpisode>>('/script_episode/create', data);
};

View File

@ -1,4 +1,5 @@
import { post } from './request';
import { ApiResponse } from './common';
// 创建剧本项目的数据类型
export interface CreateScriptProjectRequest {
@ -17,13 +18,6 @@ export interface CreateScriptProjectRequest {
resolution?: number;
}
// API响应类型
export interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
// 创建剧本项目响应数据类型
export interface ScriptProject {
id: number;

69
api/video_flow.ts Normal file
View File

@ -0,0 +1,69 @@
import { post } from './request';
import { ProjectTypeEnum } from './enums';
import { ApiResponse } from './common';
// 剧本转分镜头请求接口
export interface ScriptToSceneRequest {
script: string;
project_type: ProjectTypeEnum.SCRIPT_TO_VIDEO;
}
// 视频转分镜头请求接口
export interface VideoToSceneRequest {
video_url: string;
project_type: ProjectTypeEnum.VIDEO_TO_VIDEO;
}
// 转换分镜头请求类型
export type ConvertScenePromptRequest = ScriptToSceneRequest | VideoToSceneRequest;
// 转换分镜头响应数据接口
export interface ConvertScenePromptData {
task_id: string;
status: string;
shots_count?: number; // 剧本模式返回的分镜头数量
video_url?: string; // 视频模式返回的视频链接
estimated_time: number; // 预估处理时间(秒)
}
// 转换分镜头响应接口
export type ConvertScenePromptResponse = ApiResponse<ConvertScenePromptData>;
/**
*
* @param request - project_type
* @returns Promise<ConvertScenePromptResponse>
*/
export const convertScenePrompt = async (
request: ConvertScenePromptRequest
): Promise<ConvertScenePromptResponse> => {
return post<ConvertScenePromptResponse>('/video_flow/convert-scene-prompt', request);
};
/**
*
* @param script -
* @returns Promise<ConvertScenePromptResponse>
*/
export const convertScriptToScene = async (
script: string
): Promise<ConvertScenePromptResponse> => {
return convertScenePrompt({
script,
project_type: ProjectTypeEnum.SCRIPT_TO_VIDEO
});
};
/**
*
* @param video_url -
* @returns Promise<ConvertScenePromptResponse>
*/
export const convertVideoToScene = async (
video_url: string
): Promise<ConvertScenePromptResponse> => {
return convertScenePrompt({
video_url,
project_type: ProjectTypeEnum.VIDEO_TO_VIDEO
});
};

View File

@ -1,6 +1,5 @@
import { DashboardLayout } from '@/components/layout/dashboard-layout';
// import { HomePage } from '@/components/pages/home-page';
import { HomePage2 } from '@/components/pages/homepage';
import { HomePage2 } from '@/components/pages/home-page2';
import OAuthCallbackHandler from '@/components/ui/oauth-callback-handler';
export default function Home() {

View File

@ -8,7 +8,7 @@ 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-video.css';
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'
@ -19,11 +19,15 @@ import dynamic from 'next/dynamic';
import { ProjectTypeEnum, ModeEnum, ResolutionEnum } from "@/api/enums";
import { createScriptProject, CreateScriptProjectRequest } from "@/api/script_project";
import { createScriptEpisode, CreateScriptEpisodeRequest } from "@/api/script_episode";
import { getUploadTokenWithDomain, uploadToQiniu } from "@/api/common";
const JoyrideNoSSR = dynamic(() => import('react-joyride'), {
ssr: false,
});
// 导入Step类型
import type { Step } from 'react-joyride';
// 添加自定义滚动条样式
const scrollbarStyles = `
.custom-scrollbar::-webkit-scrollbar {
@ -55,6 +59,7 @@ export function CreateToVideo2() {
const [isClient, setIsClient] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
const [videoUrl, setVideoUrl] = useState('');
const [isUploading, setIsUploading] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const [activeTab, setActiveTab] = useState('script');
const [isFocus, setIsFocus] = useState(false);
@ -64,16 +69,33 @@ export function CreateToVideo2() {
const editorRef = useRef<HTMLDivElement>(null);
const [runTour, setRunTour] = useState(true);
const handleUploadVideo = () => {
const handleUploadVideo = async () => {
console.log('upload video');
// 打开文件选择器
const input = document.createElement('input');
input.type = 'file';
input.accept = 'video/*';
input.onchange = (e) => {
input.onchange = async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) {
setVideoUrl(URL.createObjectURL(file));
try {
setIsUploading(true);
// 获取上传token
const { token } = await getUploadTokenWithDomain();
// 上传到七牛云
const videoUrl = await uploadToQiniu(file, token);
// 上传成功设置视频URL
setVideoUrl(videoUrl);
console.log('视频上传成功:', videoUrl);
} catch (error) {
console.error('上传错误:', error);
alert('上传失败,请稍后重试');
} finally {
setIsUploading(false);
}
}
}
input.click();
@ -111,7 +133,7 @@ export function CreateToVideo2() {
alert(`创建剧集失败: ${episodeResponse.msg}`);
}
} else {
alert(`创建项目失败: ${projectResponse.message}`);
alert(`创建项目失败: ${projectResponse.msg}`);
}
} catch (error) {
alert("创建项目时发生错误,请稍后重试");

View File

@ -2,7 +2,7 @@
import { useState, useRef } from "react";
import { Plus, Table, AlignHorizontalSpaceAround, Loader2 } from "lucide-react";
import "./style/home-page.css";
import "./style/home-page2.css";
import { useRouter } from "next/navigation";
import { VideoScreenLayout } from '@/components/video-screen-layout';
import { VideoGridLayout } from '@/components/video-grid-layout';

View File

@ -0,0 +1,132 @@
.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);
}

View File

@ -1,55 +1,55 @@
.header {
pointer-events: none;
transition: color .2s;
}
@keyframes btnAnimation2 {
0% {
transform: translate(0, 0);
pointer-events: none;
transition: color .2s;
}
100% {
transform: translate(0, 100%);
@keyframes btnAnimation2 {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(0, 100%);
}
}
}
@media (pointer: fine) {
.roll.on .translate {
animation-name: btnAnimation2;
animation-play-state: running;
animation-iteration-count: 1;
animation-duration: .4s;
animation-timing-function: cubic-bezier(.16, .03, .08, 1.55);
@media (pointer: fine) {
.roll.on .translate {
animation-name: btnAnimation2;
animation-play-state: running;
animation-iteration-count: 1;
animation-duration: .4s;
animation-timing-function: cubic-bezier(.16, .03, .08, 1.55);
}
}
}
.roll .translate>span:last-child {
position: absolute;
bottom: 100%;
left: 0;
}
.logo {
display: block;
height: 100%;
object-fit: contain;
}
.header-wrapper .logo,
.roll2>span .logo,
.roll3>span .logo {
height: 22px;
}
.roll .translate {
display: inline-block;
}
.header-wrapper,
.header .link-logo {
pointer-events: initial;
overflow: hidden;
}
.header button {
pointer-events: initial;
}
.roll .translate>span:last-child {
position: absolute;
bottom: 100%;
left: 0;
}
.logo {
display: block;
height: 100%;
object-fit: contain;
}
.header-wrapper .logo,
.roll2>span .logo,
.roll3>span .logo {
height: 22px;
}
.roll .translate {
display: inline-block;
}
.header-wrapper,
.header .link-logo {
pointer-events: initial;
overflow: hidden;
}
.header button {
pointer-events: initial;
}