forked from 77media/video-flow
Merge branch 'dev' of https://git.qikongjian.com/77media/video-flow into dev
This commit is contained in:
commit
6b332afa69
@ -1,3 +1,4 @@
|
||||
NEXT_PUBLIC_JAVA_URL = https://77.app.java.auth.qikongjian.com
|
||||
NEXT_PUBLIC_BASE_URL = https://77.smartvideo.py.qikongjian.com
|
||||
# NEXT_PUBLIC_BASE_URL = https://pre.movieflow.api.huiying.video
|
||||
NEXT_PUBLIC_API_BASE_URL = https://77.api.qikongjian.com
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# NEXT_PUBLIC_BASE_URL = https://77.smartvideo.py.qikongjian.com
|
||||
NEXT_PUBLIC_BASE_URL = https://pre.movieflow.api.huiying.video
|
||||
NEXT_PUBLIC_BASE_URL = https://77.smartvideo.py.qikongjian.com
|
||||
# NEXT_PUBLIC_BASE_URL = https://pre.movieflow.api.huiying.video
|
||||
NEXT_PUBLIC_API_BASE_URL = https://77.api.qikongjian.com
|
||||
NEXT_PUBLIC_JAVA_URL = https://77.app.java.auth.qikongjian.com
|
||||
|
||||
@ -619,6 +619,10 @@ export interface RoleResponse {
|
||||
/**缓存 */
|
||||
character_draft: string;
|
||||
}
|
||||
export interface RealRoleResponse {
|
||||
system_characters: [];
|
||||
project_characters: RoleResponse[];
|
||||
}
|
||||
|
||||
|
||||
export interface Role {
|
||||
|
||||
@ -50,6 +50,7 @@ request.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
// 检查业务状态码
|
||||
if (response.data?.code !== 0 && response.data?.code !== 202) {
|
||||
console.log('response', response)
|
||||
// 处理业务层面的错误
|
||||
const businessCode = response.data?.code;
|
||||
const errorMessage = response.data?.message;
|
||||
@ -64,7 +65,6 @@ request.interceptors.response.use(
|
||||
errorHandle(4001, errorMessage);
|
||||
return Promise.reject(new Error(errorMessage));
|
||||
}
|
||||
|
||||
// 其他业务错误
|
||||
errorHandle(0, errorMessage);
|
||||
return Promise.reject(new Error(errorMessage));
|
||||
|
||||
@ -16,7 +16,7 @@ import {
|
||||
} from "@/app/service/domain/valueObject";
|
||||
import { task_item, VideoSegmentEntityAdapter } from "@/app/service/adapter/oldErrAdapter";
|
||||
import { VideoFlowProjectResponse, NewCharacterItem, NewCharacterListResponse, CharacterListByProjectWithHighlightResponse, CharacterUpdateAndRegenerateRequest, CharacterUpdateAndRegenerateResponse } from "./DTO/movieEdit";
|
||||
import { RoleResponse } from "./DTO/movieEdit";
|
||||
import { RealRoleResponse } from "./DTO/movieEdit";
|
||||
import { RoleRecognitionResponse } from "./DTO/movieEdit";
|
||||
|
||||
/**
|
||||
@ -665,6 +665,16 @@ export const regenerateShot = async (request: {
|
||||
return post("/movie/regenerate_shot_video", request);
|
||||
};
|
||||
|
||||
// 重新生成视频
|
||||
export const regenerateVideo = async (request: {
|
||||
/** 项目ID */
|
||||
project_id: string;
|
||||
/** 视频ID */
|
||||
video_id: string;
|
||||
}): Promise<ApiResponse<any>> => {
|
||||
return post("/movie_cut/regenerate_video", request);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 获取分镜列表
|
||||
@ -1165,14 +1175,14 @@ export const getSimilarCharacters = async (request: {
|
||||
/**
|
||||
* 获取项目角色列表(含高亮关键词)接口
|
||||
* @param request - 项目角色列表请求参数
|
||||
* @returns Promise<ApiResponse<RoleResponse[]>> 项目角色列表
|
||||
* @returns Promise<ApiResponse<RealRoleResponse>> 项目角色列表
|
||||
*/
|
||||
export const getCharacterListByProjectWithHighlight = async (request: {
|
||||
/** 项目ID */
|
||||
project_id: string;
|
||||
/** 每个角色最多提取的高亮关键词数量 */
|
||||
max_keywords?: number;
|
||||
}): Promise<ApiResponse<RoleResponse>> => {
|
||||
}): Promise<ApiResponse<any>> => {
|
||||
return post("/character/list_by_project_with_highlight", request);
|
||||
};
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ import { CheckCircle, XCircle, Loader2 } from "lucide-react";
|
||||
|
||||
export default function Activate() {
|
||||
const searchParams = useSearchParams();
|
||||
const t = searchParams.get("t") || '';
|
||||
const t = searchParams.get("t") || "";
|
||||
const type = searchParams.get("type");
|
||||
|
||||
if (type === "confirm_email") {
|
||||
@ -21,32 +21,40 @@ export default function Activate() {
|
||||
* @param {string} t - Verification token
|
||||
*/
|
||||
function ConfirmEmail({ t }: { t: string }) {
|
||||
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
|
||||
const [message, setMessage] = useState('');
|
||||
const [status, setStatus] = useState<"loading" | "success" | "error">(
|
||||
"loading"
|
||||
);
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!t) {
|
||||
setStatus('error');
|
||||
setMessage('Invalid verification token');
|
||||
setStatus("error");
|
||||
setMessage("Invalid verification token");
|
||||
return;
|
||||
}
|
||||
|
||||
post(`${process.env.NEXT_PUBLIC_JAVA_URL}/api/user/activate?t=${t}`)
|
||||
.then((res) => {
|
||||
setStatus('success');
|
||||
setMessage('Your registration has been verified. Please return to the official website to log in.');
|
||||
})
|
||||
.catch((err) => {
|
||||
setStatus('error');
|
||||
setMessage('Verification failed. Please try again.');
|
||||
});
|
||||
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."
|
||||
);
|
||||
}).catch((err:any) => {
|
||||
console.log('err', err)
|
||||
setStatus("error");
|
||||
setMessage("Verification failed. Please try again.");
|
||||
});
|
||||
}, [t]);
|
||||
|
||||
const renderContent = () => {
|
||||
switch (status) {
|
||||
case 'loading':
|
||||
case "loading":
|
||||
return (
|
||||
<div data-alt="loading-state" className="flex flex-col items-center justify-center space-y-4">
|
||||
<div
|
||||
data-alt="loading-state"
|
||||
className="flex flex-col items-center justify-center space-y-4"
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-cyan-400 to-purple-600 rounded-full blur-lg opacity-30 animate-pulse"></div>
|
||||
<Loader2 className="h-16 w-16 animate-spin text-cyan-400 relative z-10" />
|
||||
@ -55,52 +63,72 @@ function ConfirmEmail({ t }: { t: string }) {
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'success':
|
||||
case "success":
|
||||
return (
|
||||
<div data-alt="success-state" className="flex flex-col items-center justify-center space-y-4">
|
||||
<div
|
||||
data-alt="success-state"
|
||||
className="flex flex-col items-center justify-center space-y-4"
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-cyan-400 to-purple-600 rounded-full blur-lg opacity-30"></div>
|
||||
<CheckCircle className="h-16 w-16 text-cyan-400 relative z-10" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-semibold bg-gradient-to-r from-cyan-400 to-purple-600 bg-clip-text text-transparent">Verification Successful</h2>
|
||||
<h2 className="text-2xl font-semibold bg-gradient-to-r from-cyan-400 to-purple-600 bg-clip-text text-transparent">
|
||||
Verification Successful
|
||||
</h2>
|
||||
<p className="text-gray-300 text-center max-w-md">{message}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'error':
|
||||
case "error":
|
||||
return (
|
||||
<div data-alt="error-state" className="flex flex-col items-center justify-center space-y-4">
|
||||
<div
|
||||
data-alt="error-state"
|
||||
className="flex flex-col items-center justify-center space-y-4"
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-red-500 to-orange-500 rounded-full blur-lg opacity-30"></div>
|
||||
<XCircle className="h-16 w-16 text-red-400 relative z-10" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-semibold bg-gradient-to-r from-red-500 to-orange-500 bg-clip-text text-transparent">Verification Failed</h2>
|
||||
<h2 className="text-2xl font-semibold bg-gradient-to-r from-red-500 to-orange-500 bg-clip-text text-transparent">
|
||||
Verification Failed
|
||||
</h2>
|
||||
<p className="text-gray-300 text-center max-w-md">{message}</p>
|
||||
<button
|
||||
{/* <button
|
||||
data-alt="retry-button"
|
||||
onClick={() => window.location.reload()}
|
||||
className="mt-4 px-6 py-2 bg-gradient-to-r from-cyan-400 to-purple-600 text-white rounded-lg hover:from-cyan-500 hover:to-purple-700 transition-all duration-300 shadow-lg hover:shadow-xl transform hover:scale-105"
|
||||
>
|
||||
Retry Verification
|
||||
</button>
|
||||
</button> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-alt="email-verification-container" className="min-h-screen flex items-center justify-center bg-gradient-to-br from-black via-gray-900 to-black px-4">
|
||||
<div data-alt="verification-card" className="bg-gradient-to-br from-gray-900/90 to-black/90 backdrop-blur-sm rounded-2xl shadow-2xl p-8 max-w-md w-full border border-gray-700/50 relative overflow-hidden">
|
||||
return (
|
||||
<div
|
||||
data-alt="email-verification-container"
|
||||
className="min-h-screen flex items-center justify-center bg-gradient-to-br from-black via-gray-900 to-black px-4"
|
||||
>
|
||||
<div
|
||||
data-alt="verification-card"
|
||||
className="bg-gradient-to-br from-gray-900/90 to-black/90 backdrop-blur-sm rounded-2xl shadow-2xl p-8 max-w-md w-full border border-gray-700/50 relative overflow-hidden"
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-cyan-400/10 to-purple-600/10 pointer-events-none"></div>
|
||||
<div className="relative z-10">
|
||||
{status === 'loading' && (
|
||||
<div data-alt="verification-header" className="text-center mb-8">
|
||||
<h1 className="text-3xl font-bold text-white mb-2 bg-gradient-to-r from-cyan-400 to-purple-600 bg-clip-text text-transparent">Email Verification</h1>
|
||||
<p className="text-gray-300">Please wait while we verify your email</p>
|
||||
</div>
|
||||
)}
|
||||
{status === "loading" && (
|
||||
<div data-alt="verification-header" className="text-center mb-8">
|
||||
<h1 className="text-3xl font-bold text-white mb-2 bg-gradient-to-r from-cyan-400 to-purple-600 bg-clip-text text-transparent">
|
||||
Email Verification
|
||||
</h1>
|
||||
<p className="text-gray-300">
|
||||
Please wait while we verify your email
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{renderContent()}
|
||||
{renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -39,7 +39,7 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<title>MovieFlow - AI Film Studio</title>
|
||||
<title>MovieFlow - AI Movie Studio</title>
|
||||
<meta name="description" content="Share your story, or just a few words, and our AI turns it into a great film. We remove the barriers to creation. At MovieFlow, everyone is a movie master." />
|
||||
<meta name="robots" content="noindex" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
@ -16,37 +16,6 @@ import { DashboardLayout } from "@/components/layout/dashboard-layout";
|
||||
|
||||
export default function PricingPage() {
|
||||
|
||||
useEffect(() => {
|
||||
// 获取当前窗口尺寸
|
||||
const currentWidth = window.innerWidth;
|
||||
const currentHeight = window.innerHeight;
|
||||
// 计算缩放比例 (1920x1080)
|
||||
const wScale = currentWidth / 1920;
|
||||
const hScale = currentHeight / 1080;
|
||||
|
||||
// 检查app节点是否存在
|
||||
const pricingPage = document.getElementById("pricing-page");
|
||||
if (!pricingPage) {
|
||||
console.error("未找到app节点");
|
||||
return;
|
||||
}
|
||||
// setHPading((hScale || 1) * 10);
|
||||
// 创建样式元素
|
||||
const style = document.createElement("style");
|
||||
|
||||
// 设置CSS样式
|
||||
style.textContent = `
|
||||
#pricing-page {
|
||||
transform-origin: top left;
|
||||
transform: scale(${wScale}, ${hScale});
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
`;
|
||||
|
||||
// 将样式添加到head
|
||||
document.head.appendChild(style);
|
||||
}, []);
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<div className="w-full h-full overflow-y-auto bg-black text-white pb-[10rem]" id="pricing-page">
|
||||
@ -135,95 +104,341 @@ function HomeModule5() {
|
||||
};
|
||||
return (
|
||||
<div
|
||||
data-alt="core-value-section"
|
||||
className="home-module5 h-[1300px] relative flex flex-col items-center justify-center w-full bg-black snap-start"
|
||||
data-alt="core-value-section"
|
||||
className="home-module5 relative flex flex-col items-center justify-center w-full bg-black snap-start
|
||||
/* 移动端适配 */
|
||||
min-h-[100vh] py-8
|
||||
/* 平板适配 */
|
||||
sm:min-h-[100vh] sm:py-12
|
||||
/* 小屏笔记本适配 (13-15寸) */
|
||||
md:h-[1000px] md:py-16
|
||||
/* 大屏笔记本适配 (16-17寸) */
|
||||
lg:h-[1100px] lg:py-20
|
||||
/* 桌面端适配 (21-24寸) */
|
||||
xl:h-[1200px] xl:py-24
|
||||
/* 大屏显示器适配 (27寸+) */
|
||||
2xl:h-[1500px] 2xl:py-32"
|
||||
>
|
||||
<div
|
||||
data-alt="core-value-content"
|
||||
className="center z-10 flex flex-col items-center
|
||||
/* 移动端间距 */
|
||||
mb-8 px-4
|
||||
/* 平板间距 */
|
||||
sm:mb-12 sm:px-6
|
||||
/* 小屏笔记本间距 */
|
||||
md:mb-[4rem] md:px-8
|
||||
/* 大屏笔记本间距 */
|
||||
lg:mb-[4rem] lg:px-12
|
||||
/* 桌面端间距 */
|
||||
xl:mb-[4rem] xl:px-16
|
||||
/* 大屏显示器间距 */
|
||||
2xl:mb-[4rem] 2xl:px-20"
|
||||
>
|
||||
<div
|
||||
data-alt="core-value-content"
|
||||
className="center z-10 flex flex-col items-center mb-[4rem]"
|
||||
<h2
|
||||
className="text-white font-normal text-center
|
||||
/* 移动端字体 */
|
||||
text-[1.5rem] leading-[110%] mb-4
|
||||
/* 平板字体 */
|
||||
sm:text-[2rem] sm:leading-[110%] sm:mb-6
|
||||
/* 小屏笔记本字体 */
|
||||
md:text-[2.5rem] md:leading-[110%] md:mb-[1.5rem]
|
||||
/* 大屏笔记本字体 */
|
||||
lg:text-[3rem] lg:leading-[110%] lg:mb-[1.5rem]
|
||||
/* 桌面端字体 */
|
||||
xl:text-[3.375rem] xl:leading-[110%] xl:mb-[1.5rem]
|
||||
/* 大屏显示器字体 */
|
||||
2xl:text-[3.5rem] 2xl:leading-[110%] 2xl:mb-[1.5rem]"
|
||||
>
|
||||
<h2 className="text-white text-[3.375rem] leading-[100%] font-normal mb-[1.5rem]">
|
||||
Pick a plan and make it yours
|
||||
</h2>
|
||||
</h2>
|
||||
|
||||
{/* 计费切换 */}
|
||||
<div className="h-[3.375rem] flex bg-black rounded-full p-[0.0625rem] mt-[1.5rem] border border-white/20">
|
||||
<button
|
||||
onClick={() => setBillingType("month")}
|
||||
className={`box-border flex justify-center items-center w-[6rem] text-base rounded-full transition-all duration-300 ${
|
||||
billingType === "month"
|
||||
? "bg-white text-black"
|
||||
: "text-white hover:text-gray-300"
|
||||
}`}
|
||||
>
|
||||
Monthly
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setBillingType("year")}
|
||||
className={`box-border flex justify-center items-center w-[7.125rem] text-base rounded-full transition-all duration-300 ${
|
||||
billingType === "year"
|
||||
? "bg-white text-black"
|
||||
: "text-white hover:text-gray-300"
|
||||
}`}
|
||||
>
|
||||
Yearly - <span className="text-[#FFCC6D]">20%</span>
|
||||
</button>
|
||||
</div>
|
||||
{/* 计费切换 */}
|
||||
<div
|
||||
className="flex bg-black rounded-full border border-white/20
|
||||
/* 移动端尺寸 */
|
||||
h-[2.5rem] p-[0.0625rem] mt-4
|
||||
/* 平板尺寸 */
|
||||
sm:h-[3rem] sm:mt-6
|
||||
/* 小屏笔记本尺寸 */
|
||||
md:h-[3.375rem] md:mt-[1.5rem]
|
||||
/* 大屏笔记本尺寸 */
|
||||
lg:h-[3.375rem] lg:mt-[1.5rem]
|
||||
/* 桌面端尺寸 */
|
||||
xl:h-[3.375rem] xl:mt-[1.5rem]
|
||||
/* 大屏显示器尺寸 */
|
||||
2xl:h-[3.375rem] 2xl:mt-[1.5rem]"
|
||||
>
|
||||
<button
|
||||
onClick={() => setBillingType("month")}
|
||||
className={`box-border flex justify-center items-center rounded-full transition-all duration-300 ${
|
||||
billingType === "month"
|
||||
? "bg-white text-black"
|
||||
: "text-white hover:text-gray-300"
|
||||
}
|
||||
/* 移动端按钮尺寸 */
|
||||
w-[4.5rem] text-sm
|
||||
/* 平板按钮尺寸 */
|
||||
sm:w-[5rem] sm:text-sm
|
||||
/* 小屏笔记本按钮尺寸 */
|
||||
md:w-[6rem] md:text-base
|
||||
/* 大屏笔记本按钮尺寸 */
|
||||
lg:w-[6rem] lg:text-base
|
||||
/* 桌面端按钮尺寸 */
|
||||
xl:w-[6rem] xl:text-base
|
||||
/* 大屏显示器按钮尺寸 */
|
||||
2xl:w-[6rem] 2xl:text-base`}
|
||||
>
|
||||
Monthly
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setBillingType("year")}
|
||||
className={`box-border flex justify-center items-center rounded-full transition-all duration-300 ${
|
||||
billingType === "year"
|
||||
? "bg-white text-black"
|
||||
: "text-white hover:text-gray-300"
|
||||
}
|
||||
/* 移动端按钮尺寸 */
|
||||
w-[5.5rem] text-sm
|
||||
/* 平板按钮尺寸 */
|
||||
sm:w-[6rem] sm:text-sm
|
||||
/* 小屏笔记本按钮尺寸 */
|
||||
md:w-[7.125rem] md:text-base
|
||||
/* 大屏笔记本按钮尺寸 */
|
||||
lg:w-[7.125rem] lg:text-base
|
||||
/* 桌面端按钮尺寸 */
|
||||
xl:w-[7.125rem] xl:text-base
|
||||
/* 大屏显示器按钮尺寸 */
|
||||
2xl:w-[7.125rem] 2xl:text-base`}
|
||||
>
|
||||
Yearly - <span className="text-[#FFCC6D]">20%</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 主要价格卡片 */}
|
||||
<div className="flex justify-between w-[90%] px-[12rem] mb-[2rem]">
|
||||
{pricingPlans.map((plan, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className=" w-[24rem] h-[38.125rem] bg-black rounded-2xl p-[1.5rem] border border-white/20"
|
||||
>
|
||||
<h3 className="text-white text-2xl font-normal mb-[1rem]">
|
||||
{plan.title}
|
||||
</h3>
|
||||
<div className="mb-[1rem]">
|
||||
<span className="text-white text-[3.375rem] font-bold">
|
||||
${plan.price}
|
||||
</span>
|
||||
<span className="text-white text-xs ml-[0.5rem]">/ {billingType === "month" ? "mo" : "year"}</span>
|
||||
</div>
|
||||
<p className="text-white text-[0.875rem] mb-[1rem]">
|
||||
{plan.credits}
|
||||
</p>
|
||||
{plan.issubscribed ? (
|
||||
<button
|
||||
disabled
|
||||
className="w-full bg-gray-400 text-gray-600 py-[0.75rem] rounded-full mb-[1rem] cursor-not-allowed border border-gray-300"
|
||||
>
|
||||
Already Owned
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => handleSubscribe(plan.title)}
|
||||
className="w-full bg-white text-black py-[0.75rem] rounded-full mb-[1rem] hover:bg-black hover:text-white transition-colors border border-white/20"
|
||||
>
|
||||
{plan.buttonText}
|
||||
</button>
|
||||
)}
|
||||
<p className="w-full text-center text-white/60 text-[0.75rem] mb-[2rem]">
|
||||
* Billed monthly until cancelled
|
||||
</p>
|
||||
<ul className="space-y-[1rem]">
|
||||
{plan.features.map((feature, featureIndex) => (
|
||||
<li
|
||||
key={featureIndex}
|
||||
className="flex items-center text-white text-[0.875rem]"
|
||||
>
|
||||
<span className="text-[#C73BFF] mr-[0.5rem]">✓</span>
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{/* 主要价格卡片 */}
|
||||
<div
|
||||
className="w-full max-w-[70%] mx-auto px-4
|
||||
/* 移动端 - 单列布局 */
|
||||
grid grid-cols-1 gap-4
|
||||
/* 平板 - 双列布局 */
|
||||
sm:grid-cols-2 sm:gap-6 sm:px-6
|
||||
/* 桌面端 - 三列布局 */
|
||||
md:grid-cols-3 md:gap-8 md:px-8
|
||||
/* 大屏 - 保持三列但增加间距 */
|
||||
lg:gap-10 lg:px-12
|
||||
xl:gap-12 xl:px-16
|
||||
2xl:gap-16 2xl:px-20"
|
||||
>
|
||||
{pricingPlans.map((plan, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-black rounded-2xl border border-white/20
|
||||
/* 移动端卡片尺寸 */
|
||||
p-4 min-h-[28rem]
|
||||
/* 平板卡片尺寸 */
|
||||
sm:p-5 sm:min-h-[32rem]
|
||||
/* 小屏笔记本卡片尺寸 */
|
||||
md:p-6 md:min-h-[36rem]
|
||||
/* 大屏笔记本卡片尺寸 */
|
||||
lg:p-[1.375rem] lg:min-h-[37rem]
|
||||
/* 桌面端卡片尺寸 */
|
||||
xl:p-[1.5rem] xl:min-h-[38.125rem]
|
||||
/* 大屏显示器卡片尺寸 */
|
||||
2xl:p-[1.75rem] 2xl:min-h-[40rem]"
|
||||
>
|
||||
<h3
|
||||
className="text-white font-normal
|
||||
/* 移动端标题 */
|
||||
text-lg mb-3
|
||||
/* 平板标题 */
|
||||
sm:text-xl sm:mb-4
|
||||
/* 小屏笔记本标题 */
|
||||
md:text-xl md:mb-4
|
||||
/* 大屏笔记本标题 */
|
||||
lg:text-2xl lg:mb-[1rem]
|
||||
/* 桌面端标题 */
|
||||
xl:text-2xl xl:mb-[1rem]
|
||||
/* 大屏显示器标题 */
|
||||
2xl:text-3xl 2xl:mb-[1.25rem]"
|
||||
>
|
||||
{plan.title}
|
||||
</h3>
|
||||
<div
|
||||
className="mb-3
|
||||
/* 平板间距 */
|
||||
sm:mb-4
|
||||
/* 小屏笔记本间距 */
|
||||
md:mb-4
|
||||
/* 大屏笔记本间距 */
|
||||
lg:mb-[1rem]
|
||||
/* 桌面端间距 */
|
||||
xl:mb-[1rem]
|
||||
/* 大屏显示器间距 */
|
||||
2xl:mb-[1.25rem]"
|
||||
>
|
||||
<span
|
||||
className="text-white font-bold
|
||||
/* 移动端价格字体 */
|
||||
text-2xl
|
||||
/* 平板价格字体 */
|
||||
sm:text-3xl
|
||||
/* 小屏笔记本价格字体 */
|
||||
md:text-[2.5rem]
|
||||
/* 大屏笔记本价格字体 */
|
||||
lg:text-[3rem]
|
||||
/* 桌面端价格字体 */
|
||||
xl:text-[3.375rem]
|
||||
/* 大屏显示器价格字体 */
|
||||
2xl:text-[3.75rem]"
|
||||
>
|
||||
${plan.price}
|
||||
</span>
|
||||
<span
|
||||
className="text-white ml-2
|
||||
/* 移动端单位字体 */
|
||||
text-xs
|
||||
/* 平板单位字体 */
|
||||
sm:text-xs
|
||||
/* 小屏笔记本单位字体 */
|
||||
md:text-xs
|
||||
/* 大屏笔记本单位字体 */
|
||||
lg:text-xs
|
||||
/* 桌面端单位字体 */
|
||||
xl:text-xs
|
||||
/* 大屏显示器单位字体 */
|
||||
2xl:text-sm"
|
||||
>
|
||||
/ {billingType === "month" ? "mo" : "year"}
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
className="text-white mb-4
|
||||
/* 移动端描述字体 */
|
||||
text-sm
|
||||
/* 平板描述字体 */
|
||||
sm:text-sm
|
||||
/* 小屏笔记本描述字体 */
|
||||
md:text-[0.875rem]
|
||||
/* 大屏笔记本描述字体 */
|
||||
lg:text-[0.875rem]
|
||||
/* 桌面端描述字体 */
|
||||
xl:text-[0.875rem]
|
||||
/* 大屏显示器描述字体 */
|
||||
2xl:text-base"
|
||||
>
|
||||
{plan.credits}
|
||||
</p>
|
||||
{plan.issubscribed ? (
|
||||
<button
|
||||
disabled
|
||||
className="w-full bg-gray-400 text-gray-600 rounded-full cursor-not-allowed border border-gray-300
|
||||
/* 移动端按钮 */
|
||||
py-2 mb-4 text-sm
|
||||
/* 平板按钮 */
|
||||
sm:py-3 sm:mb-4 sm:text-base
|
||||
/* 小屏笔记本按钮 */
|
||||
md:py-[0.75rem] md:mb-[1rem] md:text-base
|
||||
/* 大屏笔记本按钮 */
|
||||
lg:py-[0.75rem] lg:mb-[1rem] lg:text-base
|
||||
/* 桌面端按钮 */
|
||||
xl:py-[0.75rem] xl:mb-[1rem] xl:text-base
|
||||
/* 大屏显示器按钮 */
|
||||
2xl:py-[0.875rem] 2xl:mb-[1.25rem] 2xl:text-lg"
|
||||
>
|
||||
Already Owned
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => handleSubscribe(plan.title)}
|
||||
className="w-full bg-white text-black rounded-full hover:bg-black hover:text-white transition-colors border border-white/20
|
||||
/* 移动端按钮 */
|
||||
py-2 mb-4 text-sm
|
||||
/* 平板按钮 */
|
||||
sm:py-3 sm:mb-4 sm:text-base
|
||||
/* 小屏笔记本按钮 */
|
||||
md:py-[0.75rem] md:mb-[1rem] md:text-base
|
||||
/* 大屏笔记本按钮 */
|
||||
lg:py-[0.75rem] lg:mb-[1rem] lg:text-base
|
||||
/* 桌面端按钮 */
|
||||
xl:py-[0.75rem] xl:mb-[1rem] xl:text-base
|
||||
/* 大屏显示器按钮 */
|
||||
2xl:py-[0.875rem] 2xl:mb-[1.25rem] 2xl:text-lg"
|
||||
>
|
||||
{plan.buttonText}
|
||||
</button>
|
||||
)}
|
||||
<p
|
||||
className="w-full text-center text-white/60 mb-4
|
||||
/* 移动端提示文字 */
|
||||
text-xs
|
||||
/* 平板提示文字 */
|
||||
sm:text-xs
|
||||
/* 小屏笔记本提示文字 */
|
||||
md:text-[0.75rem] md:mb-[2rem]
|
||||
/* 大屏笔记本提示文字 */
|
||||
lg:text-[0.75rem] lg:mb-[2rem]
|
||||
/* 桌面端提示文字 */
|
||||
xl:text-[0.75rem] xl:mb-[2rem]
|
||||
/* 大屏显示器提示文字 */
|
||||
2xl:text-sm 2xl:mb-[2.5rem]"
|
||||
>
|
||||
* Billed monthly until cancelled
|
||||
</p>
|
||||
<ul
|
||||
className="space-y-2
|
||||
/* 平板特性列表间距 */
|
||||
sm:space-y-3
|
||||
/* 小屏笔记本特性列表间距 */
|
||||
md:space-y-[1rem]
|
||||
/* 大屏笔记本特性列表间距 */
|
||||
lg:space-y-[1rem]
|
||||
/* 桌面端特性列表间距 */
|
||||
xl:space-y-[1rem]
|
||||
/* 大屏显示器特性列表间距 */
|
||||
2xl:space-y-[1.25rem]"
|
||||
>
|
||||
{plan.features.map((feature, featureIndex) => (
|
||||
<li
|
||||
key={featureIndex}
|
||||
className="flex items-center text-white
|
||||
/* 移动端特性文字 */
|
||||
text-sm
|
||||
/* 平板特性文字 */
|
||||
sm:text-sm
|
||||
/* 小屏笔记本特性文字 */
|
||||
md:text-[0.875rem]
|
||||
/* 大屏笔记本特性文字 */
|
||||
lg:text-[0.875rem]
|
||||
/* 桌面端特性文字 */
|
||||
xl:text-[0.875rem]
|
||||
/* 大屏显示器特性文字 */
|
||||
2xl:text-base"
|
||||
>
|
||||
<span
|
||||
className="text-[#C73BFF] mr-2
|
||||
/* 移动端勾号间距 */
|
||||
text-sm
|
||||
/* 平板勾号间距 */
|
||||
sm:mr-2 sm:text-base
|
||||
/* 小屏笔记本勾号间距 */
|
||||
md:mr-[0.5rem] md:text-base
|
||||
/* 大屏笔记本勾号间距 */
|
||||
lg:mr-[0.5rem] lg:text-base
|
||||
/* 桌面端勾号间距 */
|
||||
xl:mr-[0.5rem] xl:text-base
|
||||
/* 大屏显示器勾号间距 */
|
||||
2xl:mr-[0.625rem] 2xl:text-lg"
|
||||
>
|
||||
✓
|
||||
</span>
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -188,7 +188,7 @@ export const useRoleShotServiceHook = (projectId: string,selectRole?:RoleEntity,
|
||||
shot_id: shot.id, // 单个分镜ID
|
||||
character_replacements: characterReplacements,
|
||||
wait_for_completion: false, // 不等待完成,异步处理
|
||||
character_draft: JSON.stringify(newDraftRoleList)
|
||||
character_draft: newDraftRoleList ? JSON.stringify(newDraftRoleList) : ""
|
||||
})
|
||||
}))
|
||||
|
||||
|
||||
@ -80,6 +80,64 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
|
||||
}
|
||||
}, [templateStoryUseCase]);
|
||||
|
||||
/**
|
||||
* 更新指定角色的图片
|
||||
* @param {string} roleName - 角色名称
|
||||
* @param {string} imageUrl - 新的图片URL
|
||||
*/
|
||||
const updateRoleImage = useCallback(
|
||||
(roleName: string, imageUrl: string): void => {
|
||||
if (!selectedTemplate) {
|
||||
console.warn("updateRoleImage: selectedTemplate 为空");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`更新角色 ${roleName} 的图片:`, imageUrl);
|
||||
|
||||
const updatedTemplate = {
|
||||
...selectedTemplate,
|
||||
storyRole: selectedTemplate.storyRole.map((role) =>
|
||||
role.role_name === roleName
|
||||
? { ...role, photo_url: imageUrl }
|
||||
: role
|
||||
),
|
||||
};
|
||||
|
||||
console.log("更新后的模板:", updatedTemplate);
|
||||
setSelectedTemplate(updatedTemplate);
|
||||
},
|
||||
[selectedTemplate]
|
||||
);
|
||||
|
||||
/**
|
||||
* 更新指定道具的图片
|
||||
* @param {string} itemName - 道具名称
|
||||
* @param {string} imageUrl - 新的图片URL
|
||||
*/
|
||||
const updateItemImage = useCallback(
|
||||
(itemName: string, imageUrl: string): void => {
|
||||
if (!selectedTemplate) {
|
||||
console.warn("updateItemImage: selectedTemplate 为空");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`更新道具 ${itemName} 的图片:`, imageUrl);
|
||||
|
||||
const updatedTemplate = {
|
||||
...selectedTemplate,
|
||||
storyItem: selectedTemplate.storyItem.map((item) =>
|
||||
item.item_name === itemName
|
||||
? { ...item, photo_url: imageUrl }
|
||||
: item
|
||||
),
|
||||
};
|
||||
|
||||
console.log("更新后的模板:", updatedTemplate);
|
||||
setSelectedTemplate(updatedTemplate);
|
||||
},
|
||||
[selectedTemplate]
|
||||
);
|
||||
|
||||
/**
|
||||
* 上传人物头像并分析特征,替换旧的角色数据
|
||||
* @param {string} imageUrl - 图片URL
|
||||
@ -98,53 +156,7 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
/**
|
||||
* 更新指定角色的图片
|
||||
* @param {string} roleName - 角色名称
|
||||
* @param {string} imageUrl - 新的图片URL
|
||||
*/
|
||||
const updateRoleImage = useCallback(
|
||||
(roleName: string, imageUrl: string): void => {
|
||||
if (!selectedTemplate) return;
|
||||
|
||||
const updatedTemplate = {
|
||||
...selectedTemplate,
|
||||
storyRole: selectedTemplate.storyRole.map((role) =>
|
||||
role.role_name === roleName
|
||||
? { ...role, photo_url: imageUrl }
|
||||
: role
|
||||
),
|
||||
};
|
||||
|
||||
setSelectedTemplate(updatedTemplate);
|
||||
},
|
||||
[selectedTemplate]
|
||||
);
|
||||
|
||||
/**
|
||||
* 更新指定道具的图片
|
||||
* @param {string} itemName - 道具名称
|
||||
* @param {string} imageUrl - 新的图片URL
|
||||
*/
|
||||
const updateItemImage = useCallback(
|
||||
(itemName: string, imageUrl: string): void => {
|
||||
if (!selectedTemplate) return;
|
||||
|
||||
const updatedTemplate = {
|
||||
...selectedTemplate,
|
||||
storyItem: selectedTemplate.storyItem.map((item) =>
|
||||
item.item_name === itemName
|
||||
? { ...item, photo_url: imageUrl }
|
||||
: item
|
||||
),
|
||||
};
|
||||
|
||||
setSelectedTemplate(updatedTemplate);
|
||||
},
|
||||
[selectedTemplate]
|
||||
[updateRoleImage]
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@ -7,13 +7,11 @@
|
||||
- React组件(Modal、Form、Button等)
|
||||
- 样式和布局逻辑
|
||||
- 用户交互事件绑定
|
||||
- 通过props接收数据和回调函数
|
||||
|
||||
- 越是可复用组件,越要小心定义,越要简单,宁可页面中的组件使用多做点,也要组件少做点,因为复用越多,它就可能发生的变化越多,除非你很确定它的变化不会更多了。
|
||||
**设计原则**:
|
||||
- 不包含业务逻辑
|
||||
- 不直接操作状态
|
||||
- 通过回调函数与上层通信
|
||||
- 可复用和可测试
|
||||
|
||||
|
||||
### 2. Hook (状态管理层)
|
||||
**职责**:状态管理、副作用处理、业务逻辑组合
|
||||
@ -26,11 +24,29 @@
|
||||
**设计原则**:
|
||||
- 协调全局状态和本地状态
|
||||
- 处理异步操作和副作用
|
||||
- 组合多个UseCase的调用
|
||||
- 组合封装UseCase的调用,让react的方便性和hook思想体现出来
|
||||
- 提供响应式的数据接口
|
||||
|
||||
### 3. UseCase (业务逻辑层)
|
||||
**职责**:核心业务规则、业务流程编排、领域逻辑验证
|
||||
**职责**:核心业务规则、业务流程编排、领域逻辑验证,它里面的流程,应该一眼看过去,瞬间就知道这个用例中包含的业务是干啥的,里面应该是去调用一个个service,并且有注释说明,它是会最经常发生更改的,所以它要足够简单,要足够注释。
|
||||
```ts
|
||||
class DemoUseCase {
|
||||
constructor(private readonly aService: AService, private readonly bService: BService,cStore:Store) {}
|
||||
async execute(href:string,storedate:any) {
|
||||
// 获取a数据
|
||||
const aData = await this.aService.getA(href);
|
||||
// 处理b请求
|
||||
const b = await this.bService.getB(aData);
|
||||
// 更新cStore
|
||||
this.cStore.update({
|
||||
...storedate,
|
||||
b,
|
||||
});
|
||||
|
||||
return b;
|
||||
}
|
||||
}
|
||||
```
|
||||
**内容**:
|
||||
- 业务用例类(ImageStoryUseCase、ScriptGenerationUseCase等)
|
||||
- 完整的业务流程方法
|
||||
@ -43,11 +59,11 @@
|
||||
- 协调多个Service的调用
|
||||
- 返回领域实体,而不是原始数据
|
||||
- 无状态,方法调用间不依赖实例状态
|
||||
- 无关任何react的东西,纯粹的TS代码
|
||||
|
||||
### 4. Service (外部服务层)
|
||||
**职责**:外部服务集成、技术实现细节、基础设施
|
||||
**职责**:外部服务集成、技术实现细节、基础设施,它应该是一堆的服务函数,纯函数,输入到输出,没有外部的闭包依赖。不要觉得有时候可以从其它地方直接依赖,就不用参数来接收,它要干净,要是纯函数,要能通过参数判断其全部所需,所以要用接收参数的方式
|
||||
**内容**:
|
||||
- 外部API调用服务(ImageProcessingService、AIAnalysisService等)
|
||||
- 技术实现细节(图片处理、网络请求等)
|
||||
- 错误处理和重试逻辑
|
||||
- 可以被多个UseCase复用的服务
|
||||
@ -57,58 +73,41 @@
|
||||
- 处理外部API调用
|
||||
- 提供统一的错误处理
|
||||
- 实现接口隔离原则
|
||||
- 无关任何react的东西,纯粹的TS代码
|
||||
|
||||
### 5. Repository (数据访问层)
|
||||
**职责**:数据持久化、数据查询、数据转换
|
||||
**内容**:
|
||||
- 数据访问类(ImageStoryRepository、CharacterRepository等)
|
||||
- 数据持久化逻辑
|
||||
- 数据查询和过滤
|
||||
- 数据转换(Entity ↔ Data)
|
||||
|
||||
**设计原则**:
|
||||
- 封装数据访问细节
|
||||
- 提供统一的CRUD接口
|
||||
- 处理数据转换
|
||||
- 实现数据访问的抽象
|
||||
使用ApiFox去对接好后端的接口文档,然后MCP接入cursor,从而直接生成接口请求函数以及类型,相对于没有这个东西,cursor帮你写的代码将是天差地别的,能提高你大约20%的开发效率。当然这要协同后端好好写接口参数和响应的内容。
|
||||
|
||||
## 📊 状态管理策略
|
||||
|
||||
**状态分类与存储位置**:
|
||||
- **业务核心状态** → Zustand Store(currentStory、storyList等)
|
||||
- **UI交互状态** → 组件useState(isModalOpen、localInput等)
|
||||
- **业务流程状态** → UseCase私有状态(analysisProgress等)
|
||||
- **用户偏好状态** → Zustand Store(selectedCategory、theme等)
|
||||
- **临时计算状态** → useMemo/useCallback(filteredStories等)
|
||||
**状态分类**:
|
||||
- **功能业务模块状态** → 微型Store,用不同模块功能去拆Store 而非页面,记住要用单例模式处理
|
||||
- **纯UI变化状态** → 组件useState,跟业务没有任何关系,只是UI变化所用的状态。
|
||||
- **业务流程状态** → UseCase私有状态,从这里开始就是跟react任何关系都没有了,纯TS代码去写的逻辑,状态也是纯粹的业务状态,一定要在这里忘记react的存在。
|
||||
- **全局状态** → 全局Store,跨模块共享,比如用户信息、主题色、语言等。
|
||||
|
||||
## 数据流向
|
||||
|
||||
**状态流转过程**:
|
||||
用户操作 → 组件本地状态 → Hook协调 → UseCase业务逻辑 → 全局状态更新 → 组件重新渲染
|
||||
**具体流程**:
|
||||
1. 用户在组件中触发操作
|
||||
2. 组件更新本地状态
|
||||
3. Hook协调多个UseCase调用
|
||||
4. UseCase执行业务逻辑和规则验证
|
||||
5. 更新全局状态
|
||||
6. 组件重新渲染显示结果
|
||||
用户操作 → 组件本地状态 → Store Hook协调 → UseCase业务逻辑 → Store 状态更新 → 组件重新渲染
|
||||
|
||||
## 设计模式应用
|
||||
|
||||
**核心模式**:
|
||||
1. **依赖注入**:UseCase通过构造函数注入Service依赖
|
||||
2. **工厂模式**:创建UseCase实例,管理依赖关系
|
||||
3. **策略模式**:可插拔的业务策略(如不同的AI分析策略)
|
||||
4. **观察者模式**:状态变化通知,组件响应式更新
|
||||
1. **依赖注入**:UseCase通过构造函数注入Service依赖,最好的简化useCase的方式。
|
||||
2. **策略模式**:要弄清楚,什么是策略,有了这个意识,我们这个项目,很多地方可以用到策略模式,可扩展性将提高一大截
|
||||
4. **观察者模式**:我们的项目需要跨页面,跨组件的复杂通讯,采用观察者模式是非常合适的,比如chatBox.如果搞不明白这个模式,那就事件总线,这个东西更容易理解,也能实现相同功能。
|
||||
|
||||
## 🧪 测试策略
|
||||
|
||||
**分层测试**:
|
||||
- **Component层**:组件渲染和交互测试
|
||||
- **Hook层**:状态管理和副作用测试
|
||||
- **UseCase层**:业务逻辑和规则验证测试
|
||||
- **Service层**:外部服务集成测试
|
||||
- **Repository层**:数据访问逻辑测试
|
||||
- **Component层**:组件渲染和交互测试(目前的前端测试框架,测试这个纯浪费时间,不要为这个层做单元测试)
|
||||
- **Hook层**:状态管理和副作用测试(react的hook系统限制太大,不要为这个层做单元测试)
|
||||
- **UseCase层**:业务逻辑和规则验证测试(这个层需要做单元测试,最适合的地方就是这个)
|
||||
- **Service层**:外部服务集成测试(这个层不需要做单元测试,,因为用例层依赖这个层,如果用例层完备,这个就直接连带测试)
|
||||
- **Repository层**:数据访问逻辑测试(对于前端来说,就是接口请求的一堆封装,用apifox 的mcp生成接口请求函数就好,不用再封装和创建更复杂内容。若测试这个,直接apifox)
|
||||
-
|
||||
>谦卑对象模式(Humble Object Pattern)是一种设计模式,用于将复杂逻辑从难以测试的组件中分离出来,以提高代码的可测试性和可维护性。其核心思想是将与用户界面、外部系统或复杂依赖相关的代码(难以测试的部分)剥离,保留一个“谦卑”的对象,只包含简单逻辑或直接调用,而将主要业务逻辑放入易于测试的独立对象中。
|
||||
|
||||
## 错误处理机制
|
||||
|
||||
@ -120,8 +119,8 @@
|
||||
**错误处理流程**:
|
||||
1. Service层捕获原始错误并转换为应用错误
|
||||
2. UseCase层抛出领域错误
|
||||
3. Hook层统一捕获和处理错误
|
||||
4. 组件层显示错误信息
|
||||
3. Hook层将用例的错误,变成错误提示,展示给用户
|
||||
4. 组件层显示错误信息(组件层不需要做错误处理,因为组件层是纯展示层,不需要处理错误)
|
||||
|
||||
## 性能优化策略
|
||||
|
||||
@ -132,19 +131,5 @@
|
||||
|
||||
**业务逻辑优化**:
|
||||
- UseCase方法设计为无状态,避免实例状态维护
|
||||
- Service层实现缓存和重试机制
|
||||
- Repository层实现数据分页和懒加载
|
||||
|
||||
## 📋 开发检查清单
|
||||
|
||||
**新增功能时**:
|
||||
- [ ] 是否在正确的层次添加代码?
|
||||
- [ ] 是否遵循单一职责原则?
|
||||
- [ ] 是否定义了清晰的接口?
|
||||
- [ ] 是否处理了错误情况?
|
||||
- [ ] 是否添加了相应的测试?
|
||||
|
||||
**重构代码时**:
|
||||
- [ ] 是否保持了接口的向后兼容?
|
||||
- [ ] 是否更新了相关的测试?
|
||||
- [ ] 是否验证了功能完整性?
|
||||
- 应用起多个微型的Store,不再让UI组件里面出现一大堆的state,这样可以让我们放心的拆分子组件,而状态和函数可以直接使用,而不是props传递一大堆,回调一大堆,注意store只存状态,不做任何业务逻辑,业务逻辑要写在useCase里面,store中的函数是交互事件+状态修改。
|
||||
- 全局定义统一的tailwindcss的一些样式:圆角、边框、主题色、阴影。不要再像是现在这样每次都cursor随意发挥
|
||||
|
||||
@ -44,7 +44,7 @@ export class RoleEditUseCase {
|
||||
});
|
||||
|
||||
if (response.successful) {
|
||||
const roleList = this.parseProjectRoleList(response.data);
|
||||
const roleList = this.parseProjectRoleList(response.data.project_characters);
|
||||
console.log('roleList', roleList)
|
||||
return roleList;
|
||||
} else {
|
||||
|
||||
@ -19,7 +19,7 @@ export default function SignupPage() {
|
||||
const [confirmPasswordError, setConfirmPasswordError] = useState("");
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
const [agreeToTerms, setAgreeToTerms] = useState(false);
|
||||
const [agreeToTerms, setAgreeToTerms] = useState(true);
|
||||
const router = useRouter();
|
||||
|
||||
/** Password validation function with English prompts */
|
||||
@ -124,7 +124,7 @@ export default function SignupPage() {
|
||||
router.push("/login?registered=true");
|
||||
} catch (error: any) {
|
||||
console.error("Signup error:", error);
|
||||
setFormError(error.msg || "Registration failed, please try again");
|
||||
setFormError(error.message||error.msg || "Registration failed, please try again");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
@ -342,10 +342,9 @@ export default function SignupPage() {
|
||||
!!passwordError ||
|
||||
!!confirmPasswordError ||
|
||||
!password ||
|
||||
!confirmPassword ||
|
||||
!agreeToTerms
|
||||
!confirmPassword
|
||||
}
|
||||
className="flex-1 py-3 rounded-lg cursor-pointer bg-[#C039F6] hover:bg-[#C039F6]/80 text-white font-medium transition-colors disabled:opacity-70"
|
||||
className="flex-1 py-3 rounded-lg cursor-pointer bg-[#C039F6] hover:bg-[#C039F6]/80 text-white font-medium transition-colors disabled:opacity-70 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isSubmitting ? "Signing up..." : "Sign Up"}
|
||||
</button>
|
||||
@ -369,12 +368,7 @@ export default function SignupPage() {
|
||||
<div className="text-center mt-4">
|
||||
<p style={{ color: "rgba(255, 255, 255, 0.6)" }}>
|
||||
Already have an account?{" "}
|
||||
<Link
|
||||
href="/login"
|
||||
className="text-purple-400 hover:text-purple-300"
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -17,6 +17,7 @@ import {
|
||||
ImagePlay,
|
||||
Sparkles,
|
||||
Settings,
|
||||
MoreHorizontal,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Dropdown,
|
||||
@ -812,6 +813,24 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
|
||||
videoDuration: "1min",
|
||||
});
|
||||
|
||||
// 从 localStorage 初始化配置
|
||||
useEffect(() => {
|
||||
const savedConfig = localStorage.getItem('videoFlowConfig');
|
||||
if (savedConfig) {
|
||||
try {
|
||||
const parsed = JSON.parse(savedConfig);
|
||||
setConfigOptions({
|
||||
mode: parsed.mode || "auto",
|
||||
resolution: parsed.resolution || "720p",
|
||||
language: parsed.language || "english",
|
||||
videoDuration: parsed.videoDuration || "1min",
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('解析保存的配置失败,使用默认配置:', error);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 配置项显示控制状态
|
||||
const [showConfigOptions, setShowConfigOptions] = useState(false);
|
||||
|
||||
@ -918,9 +937,16 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
|
||||
<div className="bg-white/[0.08] border border-white/[0.12] rounded-lg shadow-[0_8px_32px_rgba(0,0,0,0.3)]">
|
||||
<ConfigOptions
|
||||
config={configOptions}
|
||||
onConfigChange={(key, value) =>
|
||||
setConfigOptions((prev) => ({ ...prev, [key]: value }))
|
||||
}
|
||||
onConfigChange={(key, value) => {
|
||||
setConfigOptions((prev) => {
|
||||
const newConfig = { ...prev, [key]: value };
|
||||
// 保存到 localStorage
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('videoFlowConfig', JSON.stringify(newConfig));
|
||||
}
|
||||
return newConfig;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -943,7 +969,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
|
||||
{!isExpanded && (
|
||||
<div className="flex flex-col gap-3 w-full pl-3">
|
||||
{/* 第一行:输入框 */}
|
||||
<div className="video-prompt-editor relative flex flex-col gap-3 flex-1">
|
||||
<div className="video-prompt-editor relative flex flex-col gap-3 flex-1 pr-10">
|
||||
{/* 文本输入框 - 改为textarea */}
|
||||
<textarea
|
||||
value={script}
|
||||
@ -1118,7 +1144,10 @@ const ConfigOptions = ({
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={`flex items-center p-2 gap-2`}>
|
||||
<div
|
||||
data-alt="config-options"
|
||||
className="flex items-center p-2 gap-2 bg-white/10 backdrop-blur-md rounded-lg"
|
||||
>
|
||||
{configItems.map((item) => {
|
||||
const IconComponent = item.icon;
|
||||
const currentOption = item.options.find(
|
||||
@ -1153,6 +1182,7 @@ const ConfigOptions = ({
|
||||
{currentOption?.isVip && (
|
||||
<Crown className={`w-2 h-2 text-yellow-500`} />
|
||||
)}
|
||||
<MoreHorizontal className="w-2.5 h-2.5 text-white/60" />
|
||||
</button>
|
||||
</Dropdown>
|
||||
);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { ChatMessage } from "./types";
|
||||
import { ChatMessage, MessageBlock } from "./types";
|
||||
import { bubbleVariants, hhmm } from "./utils";
|
||||
import { ProgressBar } from "./ProgressBar";
|
||||
import { Loader2, AlertCircle, CheckCircle2 } from "lucide-react";
|
||||
@ -8,9 +8,10 @@ import { Image } from 'antd';
|
||||
|
||||
interface MessageRendererProps {
|
||||
msg: ChatMessage;
|
||||
sendMessage: (blocks: MessageBlock[]) => Promise<void>;
|
||||
}
|
||||
|
||||
export function MessageRenderer({ msg }: MessageRendererProps) {
|
||||
export function MessageRenderer({ msg, sendMessage }: MessageRendererProps) {
|
||||
// Decide bubble style
|
||||
const isUser = msg.role === "user";
|
||||
const isSystem = msg.role === "system";
|
||||
@ -75,6 +76,7 @@ export function MessageRenderer({ msg }: MessageRendererProps) {
|
||||
animate="visible"
|
||||
transition={{ duration: 0.25 }}
|
||||
data-alt="message-bubble"
|
||||
key={msg.id}
|
||||
>
|
||||
<div className={`max-w-[75%] rounded-2xl shadow-md p-3 ${bubbleClass}`}>
|
||||
{/* Header */}
|
||||
@ -100,7 +102,17 @@ export function MessageRenderer({ msg }: MessageRendererProps) {
|
||||
case "image":
|
||||
return (
|
||||
<div key={idx} className="overflow-hidden rounded-xl border border-white/10">
|
||||
<Image src={b.url} alt={b.alt || "image"} className="max-h-72 object-contain w-full bg-black/10" />
|
||||
<Image
|
||||
src={b.url}
|
||||
alt={b.alt || "image"}
|
||||
className="max-h-72 object-contain w-full bg-black/10"
|
||||
preview={{
|
||||
getContainer: () => document.querySelector('[data-alt="smart-chat-box"]') || document.body,
|
||||
mask: <div className="absolute inset-0 backdrop-blur-sm bg-black/60" />,
|
||||
maskClassName: "!bg-black/60",
|
||||
rootClassName: "!z-[1000]"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case "video":
|
||||
@ -126,6 +138,11 @@ export function MessageRenderer({ msg }: MessageRendererProps) {
|
||||
return <ProgressBar key={idx} value={b.value} total={b.total} label={b.label} />;
|
||||
case "link":
|
||||
return <a key={idx} href={b.url} className="underline hover:underline text-[rgb(111 208 211)]">{b.text}</a>;
|
||||
case "applyButton":
|
||||
return <button key={idx} className="bg-[#6fd0d3] text-white px-2 py-1 rounded-md" onClick={() => {
|
||||
// 帮用户发送一条消息,消息内容是:confirm apply
|
||||
sendMessage([{ type: "text", text: "confirm apply" }]);
|
||||
}}>{b.text}</button>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useRef, useCallback, useState, useEffect } from "react";
|
||||
import { ChevronsRight, ChevronDown } from 'lucide-react';
|
||||
import { ChevronsRight, ChevronDown, X } from 'lucide-react';
|
||||
import { Switch } from 'antd';
|
||||
import { MessageRenderer } from "./MessageRenderer";
|
||||
import { InputBar } from "./InputBar";
|
||||
@ -141,7 +141,7 @@ export default function SmartChatBox({
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs opacity-70">
|
||||
<ChevronsRight
|
||||
<X
|
||||
className="w-6 h-6 cursor-pointer"
|
||||
onClick={() => setIsSmartChatBoxOpen(false)}
|
||||
/>
|
||||
@ -161,7 +161,7 @@ export default function SmartChatBox({
|
||||
<React.Fragment key={group.date}>
|
||||
<DateDivider timestamp={group.date} />
|
||||
{group.messages.map((message) => (
|
||||
<MessageRenderer key={message.id} msg={message} />
|
||||
<MessageRenderer key={message.id} msg={message} sendMessage={sendMessage} />
|
||||
))}
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
||||
@ -239,6 +239,11 @@ function transformMessage(apiMessage: RealApiMessage): ChatMessage {
|
||||
message.blocks.push({ type: "link", text: c.content, url: c.url || '' });
|
||||
}
|
||||
});
|
||||
|
||||
// todo: 需要确认是否需要添加applyButton
|
||||
// if (role === 'assistant' && function_name === 'modify_video_with_runway') {
|
||||
// message.blocks.push({ type: "applyButton", text: "Apply" });
|
||||
// }
|
||||
} catch (error) {
|
||||
// 如果 JSON 解析失败,将整个 content 作为文本内容
|
||||
message.blocks.push({ type: "text", text: content });
|
||||
|
||||
@ -8,7 +8,8 @@ export type MessageBlock =
|
||||
| { type: "video"; url: string; poster?: string }
|
||||
| { type: "audio"; url: string }
|
||||
| { type: "progress"; value: number; total?: number; label?: string }
|
||||
| { type: "link"; text: string; url: string };
|
||||
| { type: "link"; text: string; url: string }
|
||||
| { type: "applyButton"; text: string; }
|
||||
|
||||
export interface ChatMessage {
|
||||
id: string;
|
||||
@ -80,7 +81,7 @@ export interface MessagesResponse {
|
||||
}
|
||||
|
||||
type ContentType = "text" | "image" | "video" | "audio";
|
||||
export type FunctionName = "create_project" | "generate_script_summary" | "generate_character" | "generate_sketch" | "generate_shot_sketch" | "generate_video";
|
||||
export type FunctionName = "create_project" | "generate_script_summary" | "generate_character" | "generate_sketch" | "generate_shot_sketch" | "generate_video" | "modify_video_with_runway";
|
||||
|
||||
// 项目创建
|
||||
export interface ProjectInit {
|
||||
|
||||
@ -148,7 +148,7 @@ export function useMessages({ config, onMessagesUpdate }: UseMessagesProps): [Me
|
||||
try {
|
||||
// 立即添加用户消息(临时显示)
|
||||
const userMessage: ChatMessage = {
|
||||
id: uid(),
|
||||
id: Date.now().toString(),
|
||||
role: "user",
|
||||
createdAt: Date.now(),
|
||||
blocks,
|
||||
|
||||
@ -2,8 +2,10 @@
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter, usePathname } from 'next/navigation';
|
||||
import { checkAuth, getUserProfile, isAuthenticated } from '@/lib/auth';
|
||||
import { checkAuth, clearAuthData, getUserProfile, isAuthenticated } from '@/lib/auth';
|
||||
import GlobalLoad from '../common/GlobalLoad';
|
||||
import { message } from 'antd';
|
||||
import { errorHandle } from '@/api/errorHandle';
|
||||
|
||||
interface AuthGuardProps {
|
||||
children: React.ReactNode;
|
||||
@ -42,9 +44,13 @@ export default function AuthGuard({ children }: AuthGuardProps) {
|
||||
} else {
|
||||
router.push('/login');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth verification failed:', error);
|
||||
router.push('/login');
|
||||
} catch (errorCode:any) {
|
||||
// 强制的领导要求,401和502都跳转到登录页 其他的不管
|
||||
if(errorCode.message == 401||errorCode.message == 502){
|
||||
router.push('/login');
|
||||
clearAuthData();
|
||||
}
|
||||
errorHandle(errorCode.message)
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
@ -165,10 +165,13 @@ export function TopBar({ collapsed }: { collapsed: boolean }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed right-0 top-0 h-16 header z-[999]"
|
||||
style={{ isolation: "isolate", left: collapsed ? "3rem" : "16rem" }}
|
||||
>
|
||||
<div
|
||||
className="fixed right-0 top-0 h-16 header z-[999]"
|
||||
style={{
|
||||
isolation: "isolate",
|
||||
left: pathname === "/" ? "0" : (collapsed ? "2.5rem" : "16rem")
|
||||
}}
|
||||
>
|
||||
<div className="h-full flex items-center justify-between pr-6 pl-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div
|
||||
@ -207,12 +210,16 @@ export function TopBar({ collapsed }: { collapsed: boolean }) {
|
||||
</div>
|
||||
|
||||
{isLogin ? (
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
{/* Pricing Link */}
|
||||
{pathname === "/" ? (
|
||||
<div
|
||||
data-alt="go-started-button"
|
||||
className="z-100 pointer-events-auto bg-white text-black rounded-full px-4 py-2 cursor-pointer transition-opacity opacity-100 hover:opacity-80 text-sm font-medium"
|
||||
className="z-100 pointer-events-auto bg-white text-black rounded-full cursor-pointer transition-opacity opacity-100 hover:opacity-80 font-medium
|
||||
/* 移动端适配 */
|
||||
px-2 py-1.5 text-xs
|
||||
/* 平板及以上适配 */
|
||||
sm:px-4 sm:py-2 sm:text-sm"
|
||||
onClick={() => router.push("/movies")}
|
||||
>
|
||||
Go Started
|
||||
@ -263,6 +270,7 @@ export function TopBar({ collapsed }: { collapsed: boolean }) {
|
||||
data-alt="user-menu-trigger"
|
||||
style={{
|
||||
background: "unset !important",
|
||||
padding: "unset !important",
|
||||
}}
|
||||
>
|
||||
<div className="h-10 w-10 rounded-full bg-[#C73BFF] flex items-center justify-center text-white font-semibold">
|
||||
@ -288,67 +296,67 @@ export function TopBar({ collapsed }: { collapsed: boolean }) {
|
||||
data-alt="user-menu-dropdown"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<UserCard plan_name={currentUser.plan_name}>
|
||||
<div className="relative z-[2] w-full h-full flex flex-col text-white p-3">
|
||||
{/* 顶部用户信息 */}
|
||||
<div className="flex items-center space-x-3 mb-3">
|
||||
<div className="h-10 w-10 rounded-full bg-[#C73BFF] flex items-center justify-center text-white font-bold text-sm flex-shrink-0">
|
||||
{currentUser.username
|
||||
? currentUser.username.charAt(0)
|
||||
: "MF"}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-sm font-semibold text-white truncate">
|
||||
{currentUser.name || currentUser.username}
|
||||
</h3>
|
||||
<p className="text-xs text-gray-300 truncate">
|
||||
{currentUser.email}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI 积分 */}
|
||||
<div className="flex items-center justify-center space-x-3 mb-4">
|
||||
<div className="p-2 rounded-full bg-white/10 backdrop-blur-sm">
|
||||
<Sparkles className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<span className="text-white text-base font-semibold">
|
||||
{isLoadingSubscription
|
||||
? "Loading..."
|
||||
: `${credits} credits`}
|
||||
</span>
|
||||
<UserCard plan_name={currentUser.plan_name}>
|
||||
<div className="relative z-[2] w-full h-full flex flex-col text-white p-3">
|
||||
{/* 顶部用户信息 */}
|
||||
<div className="flex items-center space-x-3 mb-3">
|
||||
<div className="h-10 w-10 rounded-full bg-[#C73BFF] flex items-center justify-center text-white font-bold text-sm flex-shrink-0">
|
||||
{currentUser.username
|
||||
? currentUser.username.charAt(0)
|
||||
: "MF"}
|
||||
</div>
|
||||
|
||||
{/* 操作按钮区域 */}
|
||||
<div className="flex-1 flex flex-row justify-center items-end space-x-2 pb-2">
|
||||
<button
|
||||
className="flex-1 bg-transparent border border-white/30 text-white text-xs py-0.5 h-6 rounded hover:bg-white/10 transition-colors"
|
||||
onClick={() => {
|
||||
window.open("/pricing", "_blank");
|
||||
}}
|
||||
>
|
||||
Upgrade
|
||||
</button>
|
||||
|
||||
{currentUser.plan_name !== "none" && (
|
||||
<button
|
||||
className="flex-1 bg-transparent border border-gray-400/30 text-gray-300 text-xs py-0.5 h-6 rounded hover:bg-gray-400/10 transition-colors disabled:opacity-50"
|
||||
onClick={handleManageSubscription}
|
||||
disabled={isManagingSubscription}
|
||||
>
|
||||
Manage
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="flex-1 bg-transparent border border-red-400/50 text-red-300 text-xs py-0.5 h-6 rounded hover:bg-red-400/10 transition-colors"
|
||||
onClick={() => logoutUser()}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-sm font-semibold text-white truncate">
|
||||
{currentUser.name || currentUser.username}
|
||||
</h3>
|
||||
<p className="text-xs text-gray-300 truncate">
|
||||
{currentUser.email}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</UserCard>
|
||||
</div>
|
||||
|
||||
{/* AI 积分 */}
|
||||
<div className="flex items-center justify-center space-x-3 mb-4">
|
||||
<div className="p-2 rounded-full bg-white/10 backdrop-blur-sm">
|
||||
<Sparkles className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<span className="text-white text-base font-semibold">
|
||||
{isLoadingSubscription
|
||||
? "Loading..."
|
||||
: `${credits} credits`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮区域 */}
|
||||
<div className="flex-1 flex flex-row justify-center items-end">
|
||||
<button
|
||||
className="flex-1 bg-transparent border border-white/30 text-white text-xs py-0.5 h-6 rounded hover:bg-white/10 transition-colors"
|
||||
onClick={() => {
|
||||
window.open("/pricing", "_blank");
|
||||
}}
|
||||
>
|
||||
Upgrade
|
||||
</button>
|
||||
|
||||
{currentUser.plan_name !== "none" && (
|
||||
<button
|
||||
className="flex-1 bg-transparent border border-gray-400/30 text-gray-300 text-xs py-0.5 h-6 rounded hover:bg-gray-400/10 transition-colors disabled:opacity-50"
|
||||
onClick={handleManageSubscription}
|
||||
disabled={isManagingSubscription}
|
||||
>
|
||||
Manage
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="flex-1 bg-transparent border border-red-400/50 text-red-300 text-xs py-0.5 h-6 rounded hover:bg-red-400/10 transition-colors"
|
||||
onClick={() => logoutUser()}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</UserCard>
|
||||
</motion.div>,
|
||||
document.body
|
||||
)
|
||||
@ -356,22 +364,30 @@ export function TopBar({ collapsed }: { collapsed: boolean }) {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center space-x-4">
|
||||
<div
|
||||
data-alt="login-button"
|
||||
className="z-100 pointer-events-auto text-gray-300 hover:text-white cursor-pointer px-3 py-2 rounded transition-colors text-sm"
|
||||
onClick={() => router.push("/signup")}
|
||||
>
|
||||
Sign Up
|
||||
</div>
|
||||
<div
|
||||
data-alt="go-started-button"
|
||||
className="z-100 pointer-events-auto bg-white text-black rounded-full px-4 py-2 cursor-pointer transition-opacity opacity-100 hover:opacity-80 text-sm font-medium"
|
||||
onClick={() => router.push("/movies")}
|
||||
>
|
||||
Go Started
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 sm:space-x-4">
|
||||
<div
|
||||
data-alt="login-button"
|
||||
className="z-100 pointer-events-auto text-gray-300 hover:text-white cursor-pointer rounded transition-colors
|
||||
/* 移动端适配 */
|
||||
px-2 py-1.5 text-xs
|
||||
/* 平板及以上适配 */
|
||||
sm:px-3 sm:py-2 sm:text-sm"
|
||||
onClick={() => router.push("/signup")}
|
||||
>
|
||||
Sign Up
|
||||
</div>
|
||||
<div
|
||||
data-alt="go-started-button"
|
||||
className="z-100 pointer-events-auto bg-white text-black rounded-full cursor-pointer transition-opacity opacity-100 hover:opacity-80 font-medium
|
||||
/* 移动端适配 */
|
||||
px-2 py-1.5 text-xs
|
||||
/* 平板及以上适配 */
|
||||
sm:px-4 sm:py-2 sm:text-sm"
|
||||
onClick={() => router.push("/movies")}
|
||||
>
|
||||
Go Started
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -56,7 +56,8 @@ const WorkFlow = React.memo(function WorkFlow() {
|
||||
fallbackToStep,
|
||||
originalText,
|
||||
showGotoCutButton,
|
||||
generateEditPlan
|
||||
generateEditPlan,
|
||||
handleRetryVideo
|
||||
} = useWorkflowData();
|
||||
|
||||
const {
|
||||
@ -150,6 +151,7 @@ const WorkFlow = React.memo(function WorkFlow() {
|
||||
showGotoCutButton={showGotoCutButton}
|
||||
onGotoCut={generateEditPlan}
|
||||
isSmartChatBoxOpen={isSmartChatBoxOpen}
|
||||
onRetryVideo={(video_id) => handleRetryVideo(video_id)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -161,6 +163,7 @@ const WorkFlow = React.memo(function WorkFlow() {
|
||||
taskObject={taskObject}
|
||||
currentSketchIndex={currentSketchIndex}
|
||||
onSketchSelect={setCurrentSketchIndex}
|
||||
onRetryVideo={handleRetryVideo}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -189,7 +192,7 @@ const WorkFlow = React.memo(function WorkFlow() {
|
||||
|
||||
{/* 智能对话按钮 */}
|
||||
<div
|
||||
className="fixed right-[2rem] top-[4rem] z-[49]"
|
||||
className="fixed right-[1rem] bottom-[10rem] z-[49]"
|
||||
>
|
||||
<Tooltip title="Open chat" placement="left">
|
||||
<GlassIconButton
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import React, { useRef, useEffect, useState, SetStateAction, useMemo } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Edit3, Play, Pause, Volume2, VolumeX, Maximize, Minimize, Loader2, X, Scissors } from 'lucide-react';
|
||||
import { Edit3, Play, Pause, Volume2, VolumeX, Maximize, Minimize, Loader2, X, Scissors, RotateCcw, MessageCircleMore } from 'lucide-react';
|
||||
import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal';
|
||||
import { GlassIconButton } from '@/components/ui/glass-icon-button';
|
||||
import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer';
|
||||
@ -29,6 +29,7 @@ interface MediaViewerProps {
|
||||
showGotoCutButton?: boolean;
|
||||
onGotoCut: () => void;
|
||||
isSmartChatBoxOpen: boolean;
|
||||
onRetryVideo?: (video_id: string) => void;
|
||||
}
|
||||
|
||||
export const MediaViewer = React.memo(function MediaViewer({
|
||||
@ -47,7 +48,8 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
setVideoPreview,
|
||||
showGotoCutButton,
|
||||
onGotoCut,
|
||||
isSmartChatBoxOpen
|
||||
isSmartChatBoxOpen,
|
||||
onRetryVideo
|
||||
}: MediaViewerProps) {
|
||||
const mainVideoRef = useRef<HTMLVideoElement>(null);
|
||||
const finalVideoRef = useRef<HTMLVideoElement>(null);
|
||||
@ -318,6 +320,7 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
<div
|
||||
className="relative w-full h-full rounded-lg overflow-hidden"
|
||||
key={`render-video-${taskObject.final.url}`}
|
||||
ref={videoContentRef}
|
||||
>
|
||||
<div className="relative w-full h-full group">
|
||||
{/* 背景模糊的视频 */}
|
||||
@ -349,22 +352,23 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
{memoizedFinalVideoElement}
|
||||
</motion.div>
|
||||
|
||||
{/* 操作按钮组 */}
|
||||
{/* <AnimatePresence>
|
||||
<motion.div
|
||||
className="absolute top-4 right-4 z-10 gap-2 hidden group-hover:flex"
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
{/* 编辑和剪辑按钮 */}
|
||||
<div className="absolute top-4 z-[21] flex items-center gap-2 transition-right duration-100" style={{
|
||||
right: toosBtnRight
|
||||
}}>
|
||||
<Tooltip placement="top" title='Edit'>
|
||||
<GlassIconButton
|
||||
icon={Edit3}
|
||||
size="sm"
|
||||
onClick={() => handleEditClick('3', 'final')}
|
||||
/>
|
||||
</motion.div>
|
||||
</AnimatePresence> */}
|
||||
</Tooltip>
|
||||
{showGotoCutButton && (
|
||||
<Tooltip placement="top" title='Go to AI-powered editing platform'>
|
||||
<GlassIconButton icon={Scissors} size='sm' onClick={onGotoCut} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 底部控制区域 */}
|
||||
<motion.div
|
||||
@ -425,9 +429,15 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
{/* 生成失败 */}
|
||||
{taskObject.videos.data[currentSketchIndex].video_status === 2 && (
|
||||
<div className="absolute inset-0 bg-red-500/10 flex items-center justify-center">
|
||||
<div className="text-red-500 text-2xl font-bold flex items-center gap-2">
|
||||
<X className="w-10 h-10" />
|
||||
<span>Failed</span>
|
||||
<div
|
||||
className="text-[#813b9dcc] text-2xl font-bold flex items-center gap-2"
|
||||
>
|
||||
<RotateCcw className="w-10 h-10 cursor-pointer" onClick={() => {
|
||||
const video = taskObject.videos.data[currentSketchIndex];
|
||||
if (onRetryVideo && video?.video_id) {
|
||||
onRetryVideo(video.video_id);
|
||||
}
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -436,7 +446,7 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
|
||||
|
||||
{/* 视频 多个 取第一个 */}
|
||||
{ taskObject.videos.data[currentSketchIndex].urls && (
|
||||
{ taskObject.videos.data[currentSketchIndex].urls && taskObject.videos.data[currentSketchIndex].urls.length > 0 && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ clipPath: "inset(0 100% 0 0)" }}
|
||||
@ -452,7 +462,6 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
autoPlay={isVideoPlaying}
|
||||
loop={true}
|
||||
playsInline
|
||||
onLoadedData={() => applyVolumeSettings(mainVideoRef.current!)}
|
||||
onEnded={() => {
|
||||
if (isVideoPlaying) {
|
||||
// 自动切换到下一个视频的逻辑在父组件处理
|
||||
@ -467,7 +476,7 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
}}>
|
||||
{/* 添加到chat去编辑 按钮 */}
|
||||
<Tooltip placement="top" title="Edit video with chat">
|
||||
<GlassIconButton icon={Video} size='sm' text="Edit with chat" onClick={() => {
|
||||
<GlassIconButton icon={MessageCircleMore} size='sm' onClick={() => {
|
||||
const currentVideo = taskObject.videos.data[currentSketchIndex];
|
||||
if (currentVideo && currentVideo.urls && currentVideo.urls.length > 0 && setVideoPreview) {
|
||||
setVideoPreview(currentVideo.urls[0], currentVideo.video_id);
|
||||
|
||||
@ -249,17 +249,7 @@ export function TaskInfo({
|
||||
/>
|
||||
|
||||
{currentLoadingText === 'Task completed' ? (
|
||||
<motion.div
|
||||
className="flex items-center gap-3 justify-center"
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-5 h-5 text-emerald-500" />
|
||||
<span className="text-emerald-500 font-medium">{currentLoadingText}</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
<></>
|
||||
) : (
|
||||
<motion.div
|
||||
className="flex items-center gap-2 justify-center"
|
||||
|
||||
@ -4,7 +4,7 @@ import React, { useRef, useEffect, useState, useCallback } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal';
|
||||
import { Loader2, X, SquareUserRound, MapPinHouse, Clapperboard, Video } from 'lucide-react';
|
||||
import { Loader2, X, SquareUserRound, MapPinHouse, Clapperboard, Video, RotateCcw } from 'lucide-react';
|
||||
import { TaskObject } from '@/api/DTO/movieEdit';
|
||||
|
||||
interface ThumbnailGridProps {
|
||||
@ -12,13 +12,15 @@ interface ThumbnailGridProps {
|
||||
taskObject: TaskObject;
|
||||
currentSketchIndex: number;
|
||||
onSketchSelect: (index: number) => void;
|
||||
onRetryVideo: (video_id: string) => void;
|
||||
}
|
||||
|
||||
export function ThumbnailGrid({
|
||||
isDisabledFocus,
|
||||
taskObject,
|
||||
currentSketchIndex,
|
||||
onSketchSelect
|
||||
onSketchSelect,
|
||||
onRetryVideo
|
||||
}: ThumbnailGridProps) {
|
||||
const thumbnailsRef = useRef<HTMLDivElement>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
@ -186,19 +188,24 @@ export function ThumbnailGrid({
|
||||
)}
|
||||
{taskObject.videos.data[index].video_status === 2 && (
|
||||
<div className="absolute inset-0 bg-red-500/10 flex items-center justify-center z-20">
|
||||
<div className="text-red-500 text-xl font-bold flex items-center gap-2">
|
||||
<X className="w-10 h-10" />
|
||||
<div className="text-[#813b9dcc] text-xl font-bold flex items-center gap-2">
|
||||
<RotateCcw className="w-10 h-10 cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{taskObject.videos.data[index].urls ? (
|
||||
<video
|
||||
{taskObject.videos.data[index].urls && taskObject.videos.data[index].urls.length > 0 ? (
|
||||
// <video
|
||||
// className="w-full h-full object-cover"
|
||||
// src={taskObject.videos.data[index].urls[0]}
|
||||
// playsInline
|
||||
// loop
|
||||
// muted
|
||||
// />
|
||||
<img
|
||||
className="w-full h-full object-cover"
|
||||
src={taskObject.videos.data[index].urls[0]}
|
||||
playsInline
|
||||
loop
|
||||
muted
|
||||
src={`${taskObject.videos.data[index].urls[0]}?x-oss-process=video/snapshot,t_1000,f_jpg`}
|
||||
draggable="false"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full transform hover:scale-105 transition-transform duration-500">
|
||||
|
||||
@ -109,10 +109,11 @@ export const useEditData = (tabType: string, originalText?: string) => {
|
||||
useEffect(() => {
|
||||
console.log('useEditData-----videoSegments', videoSegments, scriptRoles);
|
||||
setShotData(videoSegments);
|
||||
setRoleData(scriptRoles);
|
||||
// setRoleData(scriptRoles);
|
||||
}, [videoSegments, scriptRoles]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('useEditData-----roleList', roleList);
|
||||
setRoleData(roleList);
|
||||
// setRoleData(mockRoleData);
|
||||
}, [roleList]);
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { detailScriptEpisodeNew, getScriptTitle, getRunningStreamData, pauseMovieProjectPlan, resumeMovieProjectPlan, getGenerateEditPlan } from '@/api/video_flow';
|
||||
import { detailScriptEpisodeNew, getScriptTitle, getRunningStreamData, pauseMovieProjectPlan, resumeMovieProjectPlan, getGenerateEditPlan, regenerateVideo } from '@/api/video_flow';
|
||||
import { useScriptService } from "@/app/service/Interaction/ScriptService";
|
||||
import { useUpdateEffect } from '@/app/hooks/useUpdateEffect';
|
||||
import { LOADING_TEXT_MAP, TaskObject, Status, Stage } from '@/api/DTO/movieEdit';
|
||||
@ -18,6 +18,8 @@ export function useWorkflowData() {
|
||||
const from = searchParams.get('from') || '';
|
||||
const token = localStorage.getItem('token') || '';
|
||||
const useid = JSON.parse(localStorage.getItem("currentUser") || '{}').id || NaN;
|
||||
// 查看缓存中 是否已经 加载过 这个项目的 剪辑计划
|
||||
const isLoaded = localStorage.getItem(`isLoaded_plan_${episodeId}`);
|
||||
|
||||
let tempTaskObject = useRef<TaskObject>({
|
||||
title: '',
|
||||
@ -113,16 +115,24 @@ export function useWorkflowData() {
|
||||
}
|
||||
}, [taskObject.currentStage]);
|
||||
|
||||
const generateEditPlan = useCallback(async () => {
|
||||
// await getGenerateEditPlan({ project_id: episodeId });
|
||||
window.open(`https://smartcut.huiying.video/ai-editor/${episodeId}?token=${token}&user_id=${useid}`, '_blank');
|
||||
const generateEditPlan = useCallback(async (isInit?: boolean) => {
|
||||
if (isLoaded) {
|
||||
return;
|
||||
}
|
||||
localStorage.setItem(`isLoaded_plan_${episodeId}`, 'true');
|
||||
isInit && await getGenerateEditPlan({ project_id: episodeId });
|
||||
openEditPlan();
|
||||
}, [episodeId]);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!from && canGoToCut) {
|
||||
// generateEditPlan();
|
||||
// }
|
||||
// }, [canGoToCut]);
|
||||
const openEditPlan = useCallback(async () => {
|
||||
window.open(`https://smartcut.movieflow.ai/ai-editor/${episodeId}?token=${token}&user_id=${useid}`, '_target');
|
||||
}, [episodeId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!from && canGoToCut && taskObject.status !== 'COMPLETED') {
|
||||
generateEditPlan(true);
|
||||
}
|
||||
}, [canGoToCut, taskObject.status]);
|
||||
|
||||
|
||||
useUpdateEffect(() => {
|
||||
@ -200,9 +210,9 @@ export function useWorkflowData() {
|
||||
// 收集所有需要更新的状态
|
||||
let stateUpdates = JSON.stringify(taskCurrent);
|
||||
// 视频分析
|
||||
let analyze_video_completed_count = all_task_data.filter((item: any) => item.task_name === 'generate_analyze_video' && item.task_status !== 'IN_PROCESS').length;
|
||||
let analyze_video_completed_count = all_task_data.filter((item: any) => item.task_name === 'generate_analyze_video' && item.task_status !== 'INIT').length;
|
||||
let analyze_video_total_count = all_task_data.filter((item: any) => item.task_name === 'generate_analyze_video').length;
|
||||
if (analyze_video_completed_count === analyze_video_total_count) {
|
||||
if (analyze_video_total_count && analyze_video_completed_count === analyze_video_total_count) {
|
||||
setCanGoToCut(true);
|
||||
}
|
||||
|
||||
@ -275,15 +285,17 @@ export function useWorkflowData() {
|
||||
taskCurrent.currentStage = 'video';
|
||||
// 正在生成视频中 替换视频数据
|
||||
const videoList = [];
|
||||
let videoUrls: string[] = [];
|
||||
let video_status = 0;
|
||||
for (const video of task.task_result.data) {
|
||||
videoUrls = video.urls ? video.urls.filter((url: null | string) => url !== null) : [];
|
||||
// 适配旧数据
|
||||
video_status = video.video_status === undefined ? (video.urls ? 1 : 0) : video.video_status;
|
||||
video_status = video.video_status === undefined ? (videoUrls.length > 0 ? 1 : 0) : video.video_status;
|
||||
// 完成 还是 0 就是 生成失败
|
||||
video_status = task.task_status === 'COMPLETED' && video_status === 0 ? 2 : video_status;
|
||||
// 每一项 video 有多个视频 先默认取第一个
|
||||
videoList.push({
|
||||
urls: video.urls,
|
||||
urls: videoUrls,
|
||||
video_id: video.video_id,
|
||||
video_status: video_status, // 0 生成中 1 生成完成 2 生成失败
|
||||
type: 'video'
|
||||
@ -452,13 +464,16 @@ export function useWorkflowData() {
|
||||
taskCurrent.currentStage = 'video';
|
||||
taskCurrent.videos.total_count = data.video.total_count;
|
||||
const videoList = [];
|
||||
let videoUrls: string[] = [];
|
||||
console.log('----------data.video.data', data.video.data);
|
||||
for (const video of data.video.data) {
|
||||
let video_status = video.video_status === undefined ? (video.urls ? 1 : 0) : video.video_status;
|
||||
videoUrls = video.urls ? video.urls.filter((url: null | string) => url !== null) : [];
|
||||
console.log('----------videoUrls', videoUrls);
|
||||
let video_status = video.video_status === undefined ? (videoUrls.length > 0 ? 1 : 0) : video.video_status;
|
||||
video_status = data.video.task_status === 'COMPLETED' && video_status === 0 ? 2 : video_status;
|
||||
// 每一项 video 有多个视频 默认取存在的项
|
||||
videoList.push({
|
||||
urls: video.urls,
|
||||
urls: videoUrls,
|
||||
video_id: video.video_id,
|
||||
video_status: video_status, // 0 生成中 1 生成完成 2 生成失败
|
||||
type: 'video'
|
||||
@ -524,6 +539,39 @@ export function useWorkflowData() {
|
||||
}
|
||||
};
|
||||
|
||||
// 重试生成视频
|
||||
const handleRetryVideo = async (video_id: string) => {
|
||||
try {
|
||||
// 先停止轮询
|
||||
await new Promise(resolve => {
|
||||
setNeedStreamData(false);
|
||||
resolve(true);
|
||||
});
|
||||
// 重置视频状态为生成中
|
||||
await new Promise(resolve => {
|
||||
setTaskObject(prev => {
|
||||
const newState = JSON.parse(JSON.stringify(prev));
|
||||
const videoIndex = newState.videos.data.findIndex((v: any) => v.video_id === video_id);
|
||||
if (videoIndex !== -1) {
|
||||
newState.videos.data[videoIndex].video_status = 0;
|
||||
}
|
||||
return newState;
|
||||
});
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
// 调用重新生成接口
|
||||
await regenerateVideo({ project_id: episodeId, video_id: video_id });
|
||||
|
||||
// 重新开启轮询
|
||||
setNeedStreamData(true);
|
||||
} catch (error) {
|
||||
console.error('重试生成视频失败:', error);
|
||||
// 发生错误时也要恢复轮询状态
|
||||
setNeedStreamData(true);
|
||||
}
|
||||
};
|
||||
|
||||
// 回退到 指定状态 重新获取数据
|
||||
const fallbackToStep = (step: string) => {
|
||||
console.log('fallbackToStep', step);
|
||||
@ -563,7 +611,8 @@ export function useWorkflowData() {
|
||||
fallbackToStep,
|
||||
originalText: state.originalText,
|
||||
// showGotoCutButton: from && currentLoadingText.includes('Post-production') ? true : false,
|
||||
showGotoCutButton: canGoToCut && currentLoadingText.includes('Post-production') ? true : false,
|
||||
generateEditPlan
|
||||
showGotoCutButton: canGoToCut ? true : false,
|
||||
generateEditPlan: openEditPlan,
|
||||
handleRetryVideo
|
||||
};
|
||||
}
|
||||
|
||||
@ -3,9 +3,9 @@ import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { SquarePen, Lightbulb, Navigation, Globe, Copy, SendHorizontal, X, Plus } from 'lucide-react';
|
||||
import { ScriptData, ScriptBlock, ScriptContent, ThemeTagBgColor, ThemeType } from './types';
|
||||
import ContentEditable, { ContentEditableEvent } from 'react-contenteditable';
|
||||
import { toast } from 'sonner';
|
||||
import { SelectDropdown } from '@/components/ui/select-dropdown';
|
||||
import { TypewriterText } from '@/components/workflow/work-office/common/TypewriterText';
|
||||
import { msg } from '@/utils/message';
|
||||
|
||||
interface ScriptRendererProps {
|
||||
data: any[];
|
||||
@ -126,11 +126,7 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data, setIsPause
|
||||
const handleThemeTagChange = (value: string[]) => {
|
||||
console.log('主题标签更改', value);
|
||||
if (value.length > 5) {
|
||||
toast.error('最多可选择5个主题标签', {
|
||||
duration: 3000,
|
||||
position: 'top-center',
|
||||
richColors: true,
|
||||
});
|
||||
msg.error('最多可选择5个主题标签', 3000);
|
||||
return;
|
||||
}
|
||||
setAddThemeTag(value);
|
||||
@ -208,6 +204,7 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data, setIsPause
|
||||
default:
|
||||
return (
|
||||
<>
|
||||
{/* 需要权限控制 */}
|
||||
<AnimatePresence>
|
||||
{(isHovered || isActive) && (
|
||||
<motion.div
|
||||
@ -219,6 +216,9 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data, setIsPause
|
||||
<SquarePen
|
||||
className="w-6 h-6 p-1 cursor-pointer text-gray-600 hover:text-blue-500 transition-colors"
|
||||
onClick={() => {
|
||||
// 提示权限不够
|
||||
msg.error('No permission!');
|
||||
return;
|
||||
handleEditBlock(block);
|
||||
}}
|
||||
/>
|
||||
@ -226,7 +226,7 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data, setIsPause
|
||||
className="w-6 h-6 p-1 cursor-pointer text-gray-600 hover:text-blue-500 transition-colors"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(block.content.map(item => item.text).join('\n'));
|
||||
toast.success('Copied!');
|
||||
msg.success('Copied!');
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
@ -237,7 +237,8 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data, setIsPause
|
||||
renderEditBlock(block)
|
||||
) : (
|
||||
block.content.map((item, index) => (
|
||||
<div key={index} onDoubleClick={() => handleEditBlock(block)}>{renderContent(item)}</div>
|
||||
// <div key={index} onDoubleClick={() => handleEditBlock(block)}>{renderContent(item)}</div>
|
||||
<div key={index}>{renderContent(item)}</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -12,6 +12,7 @@ interface CharacterEditorProps {
|
||||
highlight: TagValueObject[];
|
||||
onSmartPolish: (text: string) => void;
|
||||
onUpdateText: (text: string) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const CharacterEditor = forwardRef<any, CharacterEditorProps>(({
|
||||
@ -19,7 +20,8 @@ export const CharacterEditor = forwardRef<any, CharacterEditorProps>(({
|
||||
description,
|
||||
highlight,
|
||||
onSmartPolish,
|
||||
onUpdateText
|
||||
onUpdateText,
|
||||
disabled
|
||||
}, ref) => {
|
||||
const [isOptimizing, setIsOptimizing] = useState(false);
|
||||
const [content, setContent] = useState<any[]>([]);
|
||||
@ -70,7 +72,7 @@ export const CharacterEditor = forwardRef<any, CharacterEditorProps>(({
|
||||
<div className={cn("space-y-2 border border-white/10 relative p-2 rounded-[0.5rem] pb-12", className)}>
|
||||
{/* 自由输入区域 */}
|
||||
{
|
||||
!isInit && <MainEditor content={content} onChangeContent={handleChangeContent} />
|
||||
!isInit && <MainEditor content={content} onChangeContent={handleChangeContent} disabled={disabled} />
|
||||
}
|
||||
|
||||
{/* 智能润色按钮 */}
|
||||
|
||||
@ -12,6 +12,7 @@ import { useEditData } from '@/components/pages/work-flow/use-edit-data';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { RoleEntity } from '@/app/service/domain/Entities';
|
||||
import { Role } from '@/api/DTO/movieEdit';
|
||||
import { msg } from '@/utils/message';
|
||||
|
||||
interface CharacterTabContentProps {
|
||||
originalRoles: Role[];
|
||||
@ -112,6 +113,8 @@ CharacterTabContentProps
|
||||
|
||||
const handleSmartPolish = (text: string) => {
|
||||
// 然后调用优化角色文本
|
||||
msg.error('No permission!');
|
||||
return;
|
||||
optimizeRoleText(text);
|
||||
};
|
||||
|
||||
@ -209,6 +212,8 @@ CharacterTabContentProps
|
||||
};
|
||||
|
||||
const handleOpenReplaceLibrary = async () => {
|
||||
msg.error('No permission!');
|
||||
return;
|
||||
setIsLoadingLibrary(true);
|
||||
setIsReplaceLibraryOpen(true);
|
||||
setShowAddToLibrary(true);
|
||||
@ -217,6 +222,8 @@ CharacterTabContentProps
|
||||
};
|
||||
|
||||
const handleRegenerate = async () => {
|
||||
msg.error('No permission!');
|
||||
return;
|
||||
console.log('Regenerate');
|
||||
setIsRegenerate(true);
|
||||
// const text = characterEditorRef.current.getRoleText();
|
||||
@ -230,6 +237,8 @@ CharacterTabContentProps
|
||||
};
|
||||
|
||||
const handleUploadClick = () => {
|
||||
msg.error('No permission!');
|
||||
return;
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
@ -339,7 +348,7 @@ CharacterTabContentProps
|
||||
height='100%'
|
||||
enableAnimation={enableAnimation}
|
||||
/>
|
||||
{/* 应用角色按钮 */}
|
||||
{/* 应用角色按钮 暂时注释需要权限控制 */}
|
||||
<div className='absolute top-3 right-3 flex gap-2'>
|
||||
<motion.button
|
||||
className="p-2 bg-black/50 hover:bg-black/70
|
||||
@ -373,6 +382,7 @@ CharacterTabContentProps
|
||||
highlight={selectedRole?.tags || []}
|
||||
onSmartPolish={handleSmartPolish}
|
||||
onUpdateText={(text: string) => updateRoleText(text)}
|
||||
disabled={true}
|
||||
/>
|
||||
{/* 重新生成按钮、替换形象按钮 */}
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
|
||||
@ -13,6 +13,7 @@ import { MusicTabContent } from './music-tab-content';
|
||||
import FloatingGlassPanel from './FloatingGlassPanel';
|
||||
import { SaveEditUseCase } from '@/app/service/usecase/SaveEditUseCase';
|
||||
import { TaskObject } from '@/api/DTO/movieEdit';
|
||||
import { msg } from '@/utils/message';
|
||||
|
||||
interface EditModalProps {
|
||||
isOpen: boolean;
|
||||
@ -123,6 +124,8 @@ export function EditModal({
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
msg.error('No permission!');
|
||||
return;
|
||||
console.log('handleSave');
|
||||
// setIsRemindFallbackOpen(true);
|
||||
if (activeTab === '0') {
|
||||
@ -140,6 +143,8 @@ export function EditModal({
|
||||
}
|
||||
|
||||
const handleConfirmGotoFallback = () => {
|
||||
msg.error('No permission!');
|
||||
return;
|
||||
setDisabledBtn(true);
|
||||
console.log('handleConfirmGotoFallback');
|
||||
SaveEditUseCase.saveData();
|
||||
@ -165,6 +170,8 @@ export function EditModal({
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
msg.error('No permission!');
|
||||
return;
|
||||
console.log('handleReset');
|
||||
// 重置当前tab修改的数据
|
||||
setIsRemindResetOpen(true);
|
||||
|
||||
@ -7,9 +7,10 @@ import { HighlightTextExtension } from './HighlightText';
|
||||
interface MainEditorProps {
|
||||
content: any[];
|
||||
onChangeContent?: (content: any[]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function MainEditor({ content, onChangeContent }: MainEditorProps) {
|
||||
export default function MainEditor({ content, onChangeContent, disabled }: MainEditorProps) {
|
||||
const [renderContent, setRenderContent] = useState<any[]>(content);
|
||||
|
||||
useEffect(() => {
|
||||
@ -33,7 +34,7 @@ export default function MainEditor({ content, onChangeContent }: MainEditorProps
|
||||
},
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: 'prose prose-invert max-w-none focus:outline-none'
|
||||
class: `prose prose-invert max-w-none focus:outline-none ${disabled ? 'cursor-not-allowed' : ''}`
|
||||
},
|
||||
handleDOMEvents: {
|
||||
keydown: (view, event) => {
|
||||
@ -50,7 +51,7 @@ export default function MainEditor({ content, onChangeContent }: MainEditorProps
|
||||
}
|
||||
},
|
||||
onCreate: ({ editor }) => {
|
||||
editor.setOptions({ editable: true });
|
||||
editor.setOptions({ editable: !disabled });
|
||||
},
|
||||
onUpdate: ({ editor }) => {
|
||||
try {
|
||||
|
||||
@ -13,6 +13,7 @@ import HorizontalScroller from './HorizontalScroller';
|
||||
import { useEditData } from '@/components/pages/work-flow/use-edit-data';
|
||||
import { RoleEntity, VideoSegmentEntity } from '@/app/service/domain/Entities';
|
||||
import { ShotVideo } from '@/api/DTO/movieEdit';
|
||||
import { msg } from '@/utils/message';
|
||||
|
||||
interface ShotTabContentProps {
|
||||
currentSketchIndex: number;
|
||||
@ -444,7 +445,11 @@ export const ShotTabContent = forwardRef<
|
||||
<motion.div className='absolute top-4 right-4 flex gap-2'>
|
||||
{/* 人物替换按钮 */}
|
||||
<motion.button
|
||||
onClick={() => handleScan()}
|
||||
onClick={() => {
|
||||
msg.error('No permission!');
|
||||
return;
|
||||
handleScan()
|
||||
}}
|
||||
className={`p-2 backdrop-blur-sm transition-colors z-10 rounded-full
|
||||
${scanState === 'detected'
|
||||
? 'bg-cyan-500/50 hover:bg-cyan-500/70 text-white'
|
||||
@ -496,7 +501,11 @@ export const ShotTabContent = forwardRef<
|
||||
<span>Add Shot</span>
|
||||
</motion.button> */}
|
||||
<motion.button
|
||||
onClick={() => handleRegenerate()}
|
||||
onClick={() => {
|
||||
msg.error('No permission!');
|
||||
return;
|
||||
handleRegenerate();
|
||||
}}
|
||||
className="flex items-center justify-center gap-2 px-4 py-3 bg-blue-500/10 hover:bg-blue-500/20
|
||||
text-blue-500 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
|
||||
@ -277,7 +277,7 @@ export const getUserProfile = async (): Promise<any> => {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
throw new Error(response.status.toString());
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
@ -393,7 +393,7 @@ export const registerUser = async ({
|
||||
const data = await response.json();
|
||||
console.log('data', data)
|
||||
if(!data.success){
|
||||
throw new Error(data.msg)
|
||||
throw new Error(data.message||data.msg)
|
||||
}
|
||||
return data as {
|
||||
success: boolean;
|
||||
|
||||
@ -116,10 +116,10 @@
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/lodash": "^4.17.19",
|
||||
"@types/react-grid-layout": "^1.3.5",
|
||||
"jest": "^30.0.5",
|
||||
"ts-jest": "^29.4.0"
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
80
utils/message.tsx
Normal file
80
utils/message.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import { message } from 'antd';
|
||||
import type { ArgsProps as MessageProps } from 'antd/es/message';
|
||||
|
||||
/**
|
||||
* 全局消息提示工具
|
||||
* 对 antd message 进行封装,提供更简洁的 API
|
||||
*/
|
||||
class MessageUtil {
|
||||
/**
|
||||
* 成功提示
|
||||
* @param content - 提示内容
|
||||
* @param duration - 显示时长(秒),默认 3 秒
|
||||
* @param options - 其他配置项
|
||||
*/
|
||||
success(content: string, duration = 3, options?: Omit<MessageProps, 'content'>) {
|
||||
message.success({ content, duration, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误提示
|
||||
* @param content - 提示内容
|
||||
* @param duration - 显示时长(秒),默认 3 秒
|
||||
* @param options - 其他配置项
|
||||
*/
|
||||
error(content: string, duration = 3, options?: Omit<MessageProps, 'content'>) {
|
||||
message.error({ content, duration, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* 警告提示
|
||||
* @param content - 提示内容
|
||||
* @param duration - 显示时长(秒),默认 3 秒
|
||||
* @param options - 其他配置项
|
||||
*/
|
||||
warning(content: string, duration = 3, options?: Omit<MessageProps, 'content'>) {
|
||||
message.warning({ content, duration, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* 普通提示
|
||||
* @param content - 提示内容
|
||||
* @param duration - 显示时长(秒),默认 3 秒
|
||||
* @param options - 其他配置项
|
||||
*/
|
||||
info(content: string, duration = 3, options?: Omit<MessageProps, 'content'>) {
|
||||
message.info({ content, duration, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载提示
|
||||
* @param content - 提示内容
|
||||
* @param duration - 显示时长(秒),默认 0 表示不自动关闭
|
||||
* @param options - 其他配置项
|
||||
* @returns - 返回一个函数,调用该函数可以手动关闭提示
|
||||
*/
|
||||
loading(content: string, duration = 0, options?: Omit<MessageProps, 'content'>) {
|
||||
return message.loading({ content, duration, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁所有消息提示
|
||||
*/
|
||||
destroy() {
|
||||
message.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置全局默认值
|
||||
* @param options - 全局配置项
|
||||
*/
|
||||
config(options: MessageProps) {
|
||||
message.config(options);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
export const msg = new MessageUtil();
|
||||
|
||||
// 为了方便使用,也导出单独的方法
|
||||
export const { success, error, warning, info, loading, destroy, config } = msg;
|
||||
Loading…
x
Reference in New Issue
Block a user