forked from 77media/video-flow
Merge branch 'dev' of https://git.qikongjian.com/77media/video-flow into dev
This commit is contained in:
commit
f31331e193
@ -59,7 +59,7 @@ export default function DashboardPage() {
|
||||
const fetchPaymentDetails = async (sessionId: string) => {
|
||||
try {
|
||||
const User = JSON.parse(localStorage.getItem("currentUser") || "{}");
|
||||
const response = await fetch(`/api/payment/checkout-status/${sessionId}?user_id=${User.id}`);
|
||||
const response = await fetch(`/api/payment/checkout-status/${sessionId}?user_id=${String(User.id)}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.successful && result.data) {
|
||||
|
||||
@ -43,7 +43,7 @@ export default function PaymentSuccessPage() {
|
||||
// 使用新的Checkout Session状态查询
|
||||
const { getCheckoutSessionStatus } = await import('@/lib/stripe');
|
||||
const User = JSON.parse(localStorage.getItem("currentUser") || "{}");
|
||||
const result = await getCheckoutSessionStatus(sessionId, User.id);
|
||||
const result = await getCheckoutSessionStatus(sessionId, String(User.id));
|
||||
|
||||
if (result.successful && result.data) {
|
||||
setPaymentData(result.data);
|
||||
|
||||
@ -591,7 +591,7 @@ const RenderTemplateStoryMode = ({
|
||||
* 视频工具面板组件
|
||||
* 提供脚本输入和视频克隆两种模式,支持展开/收起功能
|
||||
*/
|
||||
export function ChatInputBox() {
|
||||
export function ChatInputBox({ noData }: { noData: boolean }) {
|
||||
// 控制面板展开/收起状态
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
@ -670,28 +670,36 @@ export function ChatInputBox() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="video-tool-component relative max-w-[1080px] w-full left-[50%] translate-x-[-50%]">
|
||||
<div className="video-tool-component relative max-w-[1080px] w-full left-[50%] translate-x-[-50%]" style={noData ? {
|
||||
top: '50%'
|
||||
} : {}}>
|
||||
{/* 视频故事板工具面板 - 毛玻璃效果背景 */}
|
||||
<div className="video-storyboard-tools rounded-[20px] bg-white/[0.08] backdrop-blur-[20px] border border-white/[0.12] shadow-[0_8px_32px_rgba(0,0,0,0.3)]">
|
||||
{/* 展开/收起控制区域 */}
|
||||
{isExpanded ? (
|
||||
// 展开状态:显示收起按钮和提示
|
||||
<div
|
||||
className="absolute top-0 bottom-0 left-0 right-0 z-[1] flex flex-col items-center justify-center rounded-[20px] bg-[#191B1E] bg-opacity-[0.3] backdrop-blur-[1.5px] cursor-pointer"
|
||||
onClick={() => setIsExpanded(false)}
|
||||
>
|
||||
<ChevronUp className="w-4 h-4 text-white/80" />
|
||||
<span className="text-sm text-white/80 mt-1">Click to action</span>
|
||||
</div>
|
||||
) : (
|
||||
// 收起状态:显示展开按钮
|
||||
<div
|
||||
className="absolute top-[-8px] left-[50%] z-[2] flex items-center justify-center w-[46px] h-4 rounded-[15px] bg-[#fff3] translate-x-[-50%] cursor-pointer hover:bg-[#ffffff1a]"
|
||||
onClick={() => setIsExpanded(true)}
|
||||
>
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
!noData && (
|
||||
<>
|
||||
{isExpanded ? (
|
||||
// 展开状态:显示收起按钮和提示
|
||||
<div
|
||||
className="absolute top-0 bottom-0 left-0 right-0 z-[1] flex flex-col items-center justify-center rounded-[20px] bg-[#191B1E] bg-opacity-[0.3] backdrop-blur-[1.5px] cursor-pointer"
|
||||
onClick={() => setIsExpanded(false)}
|
||||
>
|
||||
<ChevronUp className="w-4 h-4 text-white/80" />
|
||||
<span className="text-sm text-white/80 mt-1">Click to action</span>
|
||||
</div>
|
||||
) : (
|
||||
// 收起状态:显示展开按钮
|
||||
<div
|
||||
className="absolute top-[-8px] left-[50%] z-[2] flex items-center justify-center w-[46px] h-4 rounded-[15px] bg-[#fff3] translate-x-[-50%] cursor-pointer hover:bg-[#ffffff1a]"
|
||||
onClick={() => setIsExpanded(true)}
|
||||
>
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
{/* 主要内容区域 - 简化层级,垂直居中 */}
|
||||
<div
|
||||
@ -744,6 +752,9 @@ export function ChatInputBox() {
|
||||
onChange={(e) => setScript(e.target.value)}
|
||||
placeholder="Describe the content you want to action..."
|
||||
className="w-full pl-[10px] pr-[10px] py-[14px] rounded-[10px] leading-[20px] text-sm border-none bg-transparent text-white placeholder:text-white/[0.40] focus:outline-none resize-none min-h-[48px] max-h-[120px] overflow-y-auto"
|
||||
style={noData ? {
|
||||
minHeight: '128px'
|
||||
} : {}}
|
||||
rows={1}
|
||||
onInput={(e) => {
|
||||
const target = e.target as HTMLTextAreaElement;
|
||||
|
||||
183
components/QueueBox/QueueNotification.tsx
Normal file
183
components/QueueBox/QueueNotification.tsx
Normal file
@ -0,0 +1,183 @@
|
||||
import { notification } from 'antd';
|
||||
|
||||
const darkGlassStyle = {
|
||||
background: 'rgba(30, 32, 40, 0.95)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
WebkitBackdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.08)',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.4)',
|
||||
padding: '12px 16px',
|
||||
};
|
||||
|
||||
/** 胶片容器样式 */
|
||||
const filmStripContainerStyle = {
|
||||
position: 'relative' as const,
|
||||
width: '100%',
|
||||
height: '80px',
|
||||
marginBottom: '16px',
|
||||
overflow: 'hidden',
|
||||
};
|
||||
|
||||
/** 胶片样式 */
|
||||
const filmStripStyle = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
animation: 'filmScroll 20s linear infinite',
|
||||
};
|
||||
|
||||
/** 文字样式 */
|
||||
const textStyle = {
|
||||
fontSize: '13px',
|
||||
color: 'rgba(255, 255, 255, 0.9)',
|
||||
marginBottom: '12px',
|
||||
};
|
||||
|
||||
/** 胶片帧组件 */
|
||||
const FilmFrame = () => (
|
||||
<div style={{ margin: '0 4px' }}>
|
||||
<svg width="60" height="80" viewBox="0 0 60 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{/* 胶片外框 */}
|
||||
<rect x="0" y="0" width="60" height="80" fill="#1A1B1E" stroke="#F6B266" strokeWidth="1"/>
|
||||
{/* 齿孔 */}
|
||||
<circle cx="10" cy="5" r="3" fill="none" stroke="#F6B266" strokeWidth="1"/>
|
||||
<circle cx="50" cy="5" r="3" fill="none" stroke="#F6B266" strokeWidth="1"/>
|
||||
<circle cx="10" cy="75" r="3" fill="none" stroke="#F6B266" strokeWidth="1"/>
|
||||
<circle cx="50" cy="75" r="3" fill="none" stroke="#F6B266" strokeWidth="1"/>
|
||||
{/* 胶片画面区域 */}
|
||||
<rect x="5" y="15" width="50" height="50" fill="#2A2B2E" stroke="#F6B266" strokeWidth="1"/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
|
||||
/** 放映机音效组件 */
|
||||
const ProjectorSound = () => (
|
||||
<audio
|
||||
src="/assets/audio/projector.mp3"
|
||||
autoPlay
|
||||
loop
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
);
|
||||
|
||||
/**
|
||||
* 显示队列等待通知
|
||||
* @param position - 当前队列位置
|
||||
* @param estimatedMinutes - 预计等待分钟数
|
||||
*/
|
||||
export const showQueueNotification = (position: number, estimatedMinutes: number) => {
|
||||
notification.open({
|
||||
message: null,
|
||||
description: (
|
||||
<div data-alt="queue-notification" style={{ minWidth: '320px' }}>
|
||||
{/* 胶片动画区域 */}
|
||||
<div style={filmStripContainerStyle}>
|
||||
<div style={filmStripStyle}>
|
||||
{[...Array(6)].map((_, i) => (
|
||||
<FilmFrame key={i} />
|
||||
))}
|
||||
</div>
|
||||
<div style={filmStripStyle} className="film-strip-2">
|
||||
{[...Array(6)].map((_, i) => (
|
||||
<FilmFrame key={i} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 队列信息 */}
|
||||
<div style={textStyle}>
|
||||
<span style={{ marginRight: '8px' }}>🎬</span>
|
||||
您的作品正在第 {position} 位等待制作
|
||||
</div>
|
||||
|
||||
{/* 预计等待时间 */}
|
||||
<div style={{ ...textStyle, color: 'rgba(255, 255, 255, 0.65)' }}>
|
||||
预计等待时间:约 {estimatedMinutes} 分钟
|
||||
</div>
|
||||
|
||||
{/* 取消按钮 */}
|
||||
<button
|
||||
onClick={() => notification.destroy()}
|
||||
style={{
|
||||
color: 'rgb(250 173 20 / 90%)',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
padding: 0,
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
textDecoration: 'underline',
|
||||
textUnderlineOffset: '2px',
|
||||
textDecorationColor: 'rgb(250 173 20 / 60%)',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
data-alt="cancel-queue-button"
|
||||
>
|
||||
取消制作 →
|
||||
</button>
|
||||
|
||||
{/* 放映机音效 */}
|
||||
<ProjectorSound />
|
||||
</div>
|
||||
),
|
||||
duration: 0,
|
||||
placement: 'topRight',
|
||||
style: {
|
||||
...darkGlassStyle,
|
||||
border: '1px solid rgba(246, 178, 102, 0.2)',
|
||||
},
|
||||
className: 'movie-queue-notification',
|
||||
closeIcon: (
|
||||
<button
|
||||
className="hover:text-white"
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
padding: '2px',
|
||||
cursor: 'pointer',
|
||||
color: 'rgba(255, 255, 255, 0.45)',
|
||||
transition: 'color 0.2s ease',
|
||||
}}
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
// 添加必要的CSS动画
|
||||
const styles = `
|
||||
@keyframes filmScroll {
|
||||
0% { transform: translateX(0); }
|
||||
100% { transform: translateX(-360px); }
|
||||
}
|
||||
|
||||
.film-strip-2 {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 360px;
|
||||
}
|
||||
|
||||
.movie-queue-notification {
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
`;
|
||||
|
||||
// 将样式注入到页面
|
||||
if (typeof document !== 'undefined') {
|
||||
const styleSheet = document.createElement('style');
|
||||
styleSheet.textContent = styles;
|
||||
document.head.appendChild(styleSheet);
|
||||
}
|
||||
|
||||
// 配置通知
|
||||
notification.config({
|
||||
maxCount: 3,
|
||||
});
|
||||
238
components/QueueBox/QueueNotification2.tsx
Normal file
238
components/QueueBox/QueueNotification2.tsx
Normal file
@ -0,0 +1,238 @@
|
||||
import { notification } from 'antd';
|
||||
|
||||
const darkGlassStyle = {
|
||||
background: 'rgba(30, 32, 40, 0.95)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
WebkitBackdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.08)',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.4)',
|
||||
padding: '12px 16px',
|
||||
};
|
||||
|
||||
/** AI导演工作室容器样式 */
|
||||
const studioContainerStyle = {
|
||||
position: 'relative' as const,
|
||||
width: '100%',
|
||||
height: '120px',
|
||||
marginBottom: '16px',
|
||||
background: 'rgba(26, 27, 30, 0.6)',
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
};
|
||||
|
||||
/** AI导演组件 */
|
||||
const AIDirector = () => (
|
||||
<div className="ai-director">
|
||||
<svg width="80" height="80" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{/* AI导演的圆形头部 */}
|
||||
<circle cx="50" cy="40" r="25" fill="#F6B266"/>
|
||||
{/* 眼睛 */}
|
||||
<circle cx="40" cy="35" r="5" fill="#2A2B2E"/>
|
||||
<circle cx="60" cy="35" r="5" fill="#2A2B2E"/>
|
||||
{/* 笑容 */}
|
||||
<path d="M40 45 Q50 55 60 45" stroke="#2A2B2E" strokeWidth="3" strokeLinecap="round"/>
|
||||
{/* 导演帽 */}
|
||||
<path d="M25 30 H75 V25 H25" fill="#2A2B2E"/>
|
||||
{/* 身体 */}
|
||||
<rect x="35" y="65" width="30" height="25" fill="#F6B266"/>
|
||||
{/* 手臂 - 动画中会移动 */}
|
||||
<rect className="director-arm" x="25" y="70" width="15" height="5" fill="#F6B266"/>
|
||||
<rect className="director-arm" x="60" y="70" width="15" height="5" fill="#F6B266"/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
|
||||
/** 工作进度条组件 */
|
||||
const ProgressTimeline = () => (
|
||||
<div className="progress-timeline" style={{
|
||||
position: 'absolute',
|
||||
bottom: '10px',
|
||||
left: '20px',
|
||||
right: '20px',
|
||||
height: '4px',
|
||||
background: 'rgba(255, 255, 255, 0.1)',
|
||||
borderRadius: '2px',
|
||||
}}>
|
||||
<div className="progress-indicator" style={{
|
||||
width: '30%',
|
||||
height: '100%',
|
||||
background: '#F6B266',
|
||||
borderRadius: '2px',
|
||||
animation: 'progress 2s ease-in-out infinite',
|
||||
}}/>
|
||||
</div>
|
||||
);
|
||||
|
||||
/** 工作台元素组件 */
|
||||
const Workstation = () => (
|
||||
<div className="workstation" style={{
|
||||
position: 'absolute',
|
||||
bottom: '20px',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-around',
|
||||
}}>
|
||||
{/* 小型场景图标,会在动画中浮动 */}
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<div key={i} className={`scene-icon scene-${i}`} style={{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
background: 'rgba(246, 178, 102, 0.3)',
|
||||
borderRadius: '4px',
|
||||
animation: `float ${1 + i * 0.5}s ease-in-out infinite alternate`,
|
||||
}}/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* 显示队列等待通知
|
||||
* @param position - 当前队列位置
|
||||
* @param estimatedMinutes - 预计等待分钟数
|
||||
*/
|
||||
export const showQueueNotification = (position: number, estimatedMinutes: number) => {
|
||||
notification.open({
|
||||
message: null,
|
||||
description: (
|
||||
<div data-alt="queue-notification" style={{ minWidth: '320px' }}>
|
||||
{/* AI导演工作室场景 */}
|
||||
<div style={studioContainerStyle}>
|
||||
<AIDirector />
|
||||
<Workstation />
|
||||
<ProgressTimeline />
|
||||
</div>
|
||||
|
||||
{/* 队列信息 */}
|
||||
<div style={{
|
||||
fontSize: '13px',
|
||||
color: 'rgba(255, 255, 255, 0.9)',
|
||||
marginBottom: '12px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
background: 'rgba(246, 178, 102, 0.1)',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '6px',
|
||||
}}>
|
||||
<span style={{ marginRight: '8px' }}>🎬</span>
|
||||
您的作品正在第 {position} 位等待制作
|
||||
</div>
|
||||
|
||||
{/* 预计等待时间 */}
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: 'rgba(255, 255, 255, 0.65)',
|
||||
marginBottom: '12px',
|
||||
}}>
|
||||
预计等待时间:约 {estimatedMinutes} 分钟
|
||||
</div>
|
||||
|
||||
{/* 取消按钮 */}
|
||||
<button
|
||||
onClick={() => notification.destroy()}
|
||||
style={{
|
||||
color: 'rgb(250 173 20 / 90%)',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
padding: 0,
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
textDecoration: 'underline',
|
||||
textUnderlineOffset: '2px',
|
||||
textDecorationColor: 'rgb(250 173 20 / 60%)',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
data-alt="cancel-queue-button"
|
||||
>
|
||||
取消制作 →
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
duration: 0,
|
||||
placement: 'topRight',
|
||||
style: {
|
||||
...darkGlassStyle,
|
||||
border: '1px solid rgba(246, 178, 102, 0.2)',
|
||||
},
|
||||
className: 'director-studio-notification',
|
||||
closeIcon: (
|
||||
<button
|
||||
className="hover:text-white"
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
padding: '2px',
|
||||
cursor: 'pointer',
|
||||
color: 'rgba(255, 255, 255, 0.45)',
|
||||
transition: 'color 0.2s ease',
|
||||
}}
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
// 添加必要的CSS动画
|
||||
const styles = `
|
||||
.ai-director {
|
||||
animation: bounce 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.director-arm {
|
||||
transform-origin: center;
|
||||
animation: wave 1s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-5px); }
|
||||
}
|
||||
|
||||
@keyframes wave {
|
||||
0% { transform: rotate(-5deg); }
|
||||
100% { transform: rotate(5deg); }
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0% { transform: translateY(0); }
|
||||
100% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
@keyframes progress {
|
||||
0% { width: 0%; }
|
||||
50% { width: 60%; }
|
||||
100% { width: 30%; }
|
||||
}
|
||||
|
||||
.director-studio-notification {
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
.scene-0 { animation-delay: 0s; }
|
||||
.scene-1 { animation-delay: 0.2s; }
|
||||
.scene-2 { animation-delay: 0.4s; }
|
||||
`;
|
||||
|
||||
// 将样式注入到页面
|
||||
if (typeof document !== 'undefined') {
|
||||
const styleSheet = document.createElement('style');
|
||||
styleSheet.textContent = styles;
|
||||
document.head.appendChild(styleSheet);
|
||||
}
|
||||
|
||||
// 配置通知
|
||||
notification.config({
|
||||
maxCount: 3,
|
||||
});
|
||||
209
components/QueueBox/queue-notification.tsx
Normal file
209
components/QueueBox/queue-notification.tsx
Normal file
@ -0,0 +1,209 @@
|
||||
import { notification } from 'antd';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
type NotificationType = 'success' | 'info' | 'warning' | 'error';
|
||||
|
||||
const darkGlassStyle = {
|
||||
background: 'rgba(30, 32, 40, 0.95)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
WebkitBackdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.08)',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.4)',
|
||||
padding: '12px 16px',
|
||||
};
|
||||
|
||||
const messageStyle = {
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
color: '#ffffff',
|
||||
marginBottom: '6px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
};
|
||||
|
||||
const iconStyle = {
|
||||
color: '#F6B266', // 警告图标颜色
|
||||
background: 'rgba(246, 178, 102, 0.15)',
|
||||
padding: '4px',
|
||||
borderRadius: '6px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
};
|
||||
|
||||
const descriptionStyle = {
|
||||
fontSize: '12px',
|
||||
color: 'rgba(255, 255, 255, 0.65)',
|
||||
marginBottom: '12px',
|
||||
lineHeight: '1.5',
|
||||
};
|
||||
|
||||
const btnStyle = {
|
||||
color: 'rgb(250 173 20 / 90%)',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
padding: 0,
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
textDecoration: 'underline',
|
||||
textUnderlineOffset: '2px',
|
||||
textDecorationColor: 'rgb(250 173 20 / 60%)',
|
||||
transition: 'all 0.2s ease',
|
||||
};
|
||||
|
||||
/** 场记板动画样式 */
|
||||
const clapperboardStyle = {
|
||||
position: 'relative' as const,
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
marginRight: '12px',
|
||||
animation: 'clap 2s infinite',
|
||||
};
|
||||
|
||||
/** 场记板文字样式 */
|
||||
const sceneTextStyle = {
|
||||
fontSize: '14px',
|
||||
fontFamily: 'monospace',
|
||||
color: '#F6B266',
|
||||
marginBottom: '8px',
|
||||
letterSpacing: '0.5px',
|
||||
};
|
||||
|
||||
/** 队列信息样式 */
|
||||
const queueInfoStyle = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontSize: '13px',
|
||||
color: 'rgba(255, 255, 255, 0.9)',
|
||||
marginBottom: '12px',
|
||||
background: 'rgba(246, 178, 102, 0.1)',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '6px',
|
||||
};
|
||||
|
||||
/**
|
||||
* 场记板SVG组件
|
||||
*/
|
||||
const Clapperboard = () => (
|
||||
<div style={clapperboardStyle}>
|
||||
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 4H20V18C20 19.1046 19.1046 20 18 20H6C4.89543 20 4 19.1046 4 18V4Z"
|
||||
stroke="#F6B266" strokeWidth="1.5"/>
|
||||
<path d="M4 8H20" stroke="#F6B266" strokeWidth="1.5"/>
|
||||
<path d="M9 4L11 8M15 4L17 8" stroke="#F6B266" strokeWidth="1.5"/>
|
||||
<path className="clap-top" d="M4 4L20 4L17 8L4 8L4 4Z"
|
||||
fill="rgba(246, 178, 102, 0.2)" stroke="#F6B266" strokeWidth="1.5"/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* 显示队列等待通知
|
||||
* @param position - 当前队列位置
|
||||
* @param estimatedMinutes - 预计等待分钟数
|
||||
*/
|
||||
export const showQueueNotification = (position: number, estimatedMinutes: number) => {
|
||||
// 生成场景号和镜次号
|
||||
const sceneNumber = Math.floor(Math.random() * 5) + 1;
|
||||
const takeNumber = Math.floor(Math.random() * 3) + 1;
|
||||
|
||||
notification.open({
|
||||
message: null,
|
||||
description: (
|
||||
<div data-alt="queue-notification" style={{ minWidth: '320px' }}>
|
||||
{/* 场记板和场景信息 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '12px' }}>
|
||||
<Clapperboard />
|
||||
<div>
|
||||
<div style={sceneTextStyle}>
|
||||
Scene {sceneNumber} - Take {takeNumber}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: 'rgba(255, 255, 255, 0.6)' }}>
|
||||
AI Director's Cut
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 队列信息 */}
|
||||
<div style={queueInfoStyle}>
|
||||
<span style={{ marginRight: '8px' }}>🎬</span>
|
||||
您的作品正在第 {position} 位等待制作
|
||||
</div>
|
||||
|
||||
{/* 预计等待时间 */}
|
||||
<div style={descriptionStyle}>
|
||||
预计等待时间:约 {estimatedMinutes} 分钟
|
||||
</div>
|
||||
|
||||
{/* 取消按钮 */}
|
||||
<button
|
||||
onClick={() => notification.destroy()}
|
||||
style={{
|
||||
...btnStyle,
|
||||
marginTop: '8px',
|
||||
}}
|
||||
data-alt="cancel-queue-button"
|
||||
>
|
||||
取消制作 →
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
duration: 0, // 保持通知直到用户关闭
|
||||
placement: 'topRight',
|
||||
style: {
|
||||
...darkGlassStyle,
|
||||
border: '1px solid rgba(246, 178, 102, 0.2)',
|
||||
},
|
||||
className: 'movie-queue-notification',
|
||||
closeIcon: (
|
||||
<button
|
||||
className="hover:text-white"
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
padding: '2px',
|
||||
cursor: 'pointer',
|
||||
color: 'rgba(255, 255, 255, 0.45)',
|
||||
transition: 'color 0.2s ease',
|
||||
}}
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
// 添加必要的CSS动画
|
||||
const styles = `
|
||||
@keyframes clap {
|
||||
0%, 100% { transform: rotate(0deg); }
|
||||
5% { transform: rotate(-15deg); }
|
||||
10% { transform: rotate(0deg); }
|
||||
}
|
||||
|
||||
.movie-queue-notification {
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
`;
|
||||
|
||||
// 将样式注入到页面
|
||||
if (typeof document !== 'undefined') {
|
||||
const styleSheet = document.createElement('style');
|
||||
styleSheet.textContent = styles;
|
||||
document.head.appendChild(styleSheet);
|
||||
}
|
||||
|
||||
// 保持现有的notification配置
|
||||
notification.config({
|
||||
maxCount: 3,
|
||||
});
|
||||
@ -13,7 +13,6 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
|
||||
return (
|
||||
<div className=" min-h-screen bg-background">
|
||||
<Sidebar collapsed={sidebarCollapsed} onToggle={setSidebarCollapsed} />
|
||||
<TopBar collapsed={sidebarCollapsed} onToggleSidebar={() => setSidebarCollapsed(!sidebarCollapsed)} />
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@ -11,12 +11,15 @@ import {
|
||||
Sparkles,
|
||||
LogOut,
|
||||
PanelsLeftBottom,
|
||||
} from "lucide-react";
|
||||
import { motion } from "framer-motion";
|
||||
import ReactDOM from "react-dom";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import React, { useRef, useEffect, useLayoutEffect, useState } from "react";
|
||||
import { logoutUser } from "@/lib/auth";
|
||||
Bell,
|
||||
} from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { useRouter, usePathname } from 'next/navigation';
|
||||
import React, { useRef, useEffect, useLayoutEffect, useState } from 'react';
|
||||
import { logoutUser } from '@/lib/auth';
|
||||
import { createPortalSession, redirectToPortal } from '@/lib/stripe';
|
||||
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
@ -42,6 +45,7 @@ export function TopBar({
|
||||
const [mounted, setMounted] = React.useState(false);
|
||||
const [isLogin, setIsLogin] = useState(false);
|
||||
const pathname = usePathname();
|
||||
const [isManagingSubscription, setIsManagingSubscription] = useState(false);
|
||||
useEffect(() => {
|
||||
const currentUser = localStorage.getItem("currentUser");
|
||||
if (JSON.parse(currentUser || "{}")?.token) {
|
||||
@ -56,6 +60,34 @@ export function TopBar({
|
||||
return () => console.log("Cleanup mounted effect");
|
||||
}, []);
|
||||
|
||||
// 处理订阅管理
|
||||
const handleManageSubscription = async () => {
|
||||
if (!currentUser?.id) {
|
||||
console.error('用户未登录');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsManagingSubscription(true);
|
||||
try {
|
||||
const response = await createPortalSession({
|
||||
user_id: String(currentUser.id),
|
||||
return_url: window.location.origin + '/dashboard'
|
||||
});
|
||||
|
||||
if (response.successful && response.data?.portal_url) {
|
||||
redirectToPortal(response.data.portal_url);
|
||||
} else {
|
||||
console.error('创建订阅管理会话失败:', response.message);
|
||||
alert('无法打开订阅管理页面,请稍后重试');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('打开订阅管理页面失败:', error);
|
||||
alert('无法打开订阅管理页面,请稍后重试');
|
||||
} finally {
|
||||
setIsManagingSubscription(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理点击事件
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
@ -109,18 +141,8 @@ export function TopBar({
|
||||
>
|
||||
<div className="h-full flex items-center justify-between pr-6 pl-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
{isLogin && (
|
||||
<Button
|
||||
className="button-NxtqWZ"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onToggleSidebar}
|
||||
>
|
||||
<PanelsLeftBottom className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
<div
|
||||
className={`flex items-center cursor-pointer space-x-4 link-logo roll event-on`}
|
||||
className={`flex items-center cursor-pointer space-x-1 link-logo roll event-on`}
|
||||
onClick={() => router.push("/")}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onAnimationEnd={handleAnimationEnd}
|
||||
@ -169,7 +191,7 @@ export function TopBar({
|
||||
</Button>
|
||||
|
||||
{/* Notifications */}
|
||||
{/* <Button variant="ghost" size="sm" onClick={() => setOpenModal(true)}>
|
||||
{/* <Button variant="ghost" size="sm" onClick={() => showQueueNotification(3, 10)}>
|
||||
<Bell className="h-4 w-4" />
|
||||
</Button> */}
|
||||
|
||||
@ -265,9 +287,8 @@ export function TopBar({
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-white border-white hover:bg-white/10 rounded-full px-3 py-1 text-[10px]"
|
||||
onClick={() => {
|
||||
window.open("/pricing", "_blank");
|
||||
}}
|
||||
onClick={handleManageSubscription}
|
||||
disabled={isManagingSubscription}
|
||||
>
|
||||
Manage
|
||||
</Button>
|
||||
|
||||
@ -8,6 +8,8 @@ import './style/create-to-video2.css';
|
||||
import { createScriptEpisodeNew, getScriptEpisodeListNew, CreateScriptEpisodeRequest } from "@/api/script_episode";
|
||||
import { EmptyStateAnimation } from '@/components/common/EmptyStateAnimation2';
|
||||
import { ChatInputBox } from '@/components/ChatInputBox/ChatInputBox';
|
||||
import cover_image1 from '@/public/assets/cover_image1.jpg';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
|
||||
// ideaText已迁移到ChatInputBox组件中
|
||||
@ -98,6 +100,136 @@ export default function CreateToVideo2() {
|
||||
setIsClient(true);
|
||||
}, []);
|
||||
|
||||
const StatusBadge = (status: string) => {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
className="flex items-center gap-2 rounded-full
|
||||
bg-white/10 border border-white/20
|
||||
px-3 py-1 backdrop-blur-md shadow-[0_0_8px_rgba(255,255,255,0.3)]"
|
||||
>
|
||||
{/* 进行中 脉冲小圆点 */}
|
||||
{status === 'pending' && (
|
||||
<>
|
||||
<motion.span
|
||||
className="w-2 h-2 rounded-full bg-yellow-400 shadow-[0_0_8px_rgba(255,220,100,0.9)]"
|
||||
animate={{ scale: [1, 1.4, 1] }}
|
||||
transition={{ repeat: Infinity, duration: 1.5 }}
|
||||
/>
|
||||
{/* 状态文字 */}
|
||||
<span className="text-xs tracking-widest text-yellow-300 font-medium drop-shadow-[0_0_6px_rgba(255,220,100,0.6)]">
|
||||
PROCESSING
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{/* 已完成 */}
|
||||
{status === 'completed' && (
|
||||
<>
|
||||
<motion.span
|
||||
className="w-2 h-2 rounded-full bg-green-400 shadow-[0_0_8px_rgba(0,255,120,0.9)]"
|
||||
/>
|
||||
<span className="text-xs tracking-widest text-green-300 font-medium drop-shadow-[0_0_6px_rgba(0,255,120,0.6)]">
|
||||
COMPLETED
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{/* 失败 */}
|
||||
{status === 'failed' && (
|
||||
<>
|
||||
<motion.span
|
||||
className="w-2 h-2 rounded-full bg-red-500 shadow-[0_0_8px_rgba(255,0,80,0.9)]"
|
||||
/>
|
||||
<span className="text-xs tracking-widest text-red-400 font-medium drop-shadow-[0_0_6px_rgba(255,0,80,0.6)]">
|
||||
FAILED
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
// 创建一个视频引用Map
|
||||
const videoRefs = useRef<Map<string, HTMLVideoElement>>(new Map());
|
||||
|
||||
const handleMouseEnter = (projectId: string) => {
|
||||
const videoElement = videoRefs.current.get(projectId);
|
||||
if (videoElement) {
|
||||
videoElement.play().catch(() => {
|
||||
console.log('Video autoplay prevented');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = (projectId: string) => {
|
||||
const videoElement = videoRefs.current.get(projectId);
|
||||
if (videoElement) {
|
||||
videoElement.pause();
|
||||
videoElement.currentTime = 0;
|
||||
}
|
||||
};
|
||||
|
||||
const setVideoRef = (projectId: string, element: HTMLVideoElement | null) => {
|
||||
if (element) {
|
||||
videoRefs.current.set(projectId, element);
|
||||
} else {
|
||||
videoRefs.current.delete(projectId);
|
||||
}
|
||||
};
|
||||
|
||||
const renderProjectCard = (project: any) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
key={project.project_id}
|
||||
className="group relative aspect-video bg-black/20 rounded-lg overflow-hidden cursor-pointer"
|
||||
onClick={() => router.push(`/create/work-flow?episodeId=${project.project_id}`)}
|
||||
onMouseEnter={() => handleMouseEnter(project.project_id)}
|
||||
onMouseLeave={() => handleMouseLeave(project.project_id)}
|
||||
data-alt="project-card"
|
||||
>
|
||||
{/* 视频/图片区域 */}
|
||||
{project.final_video_url ? (
|
||||
<video
|
||||
ref={(el) => setVideoRef(project.project_id, el)}
|
||||
src={project.final_video_url}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
preload="none"
|
||||
poster={`${project.final_video_url}?vframe/jpg/offset/1`}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center group-hover:scale-105 transition-transform duration-500"
|
||||
style={{
|
||||
backgroundImage: `url(${cover_image1.src})`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 渐变遮罩 */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent" />
|
||||
|
||||
{/* 状态标签 - 左上角 */}
|
||||
<div className="absolute top-3 left-3">
|
||||
{StatusBadge(project.status === 'COMPLETED' ? 'completed' : project.status === 'FAILED' ? 'failed' : 'pending')}
|
||||
</div>
|
||||
|
||||
{/* 底部信息 */}
|
||||
<div className="absolute bottom-0 left-0 right-0 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-medium text-white line-clamp-1">
|
||||
{project.name || "Unnamed"}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染剧集卡片
|
||||
const renderEpisodeCard = (episode: any) => {
|
||||
return (
|
||||
@ -190,12 +322,12 @@ export default function CreateToVideo2() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="container mx-auto h-[calc(100vh-6rem)] flex flex-col absolute top-[4rem] left-0 right-0 px-[1rem]">
|
||||
<div className="flex flex-col absolute top-[5rem] left-0 right-0 bottom-[1rem] px-6">
|
||||
{/* 优化后的主要内容区域 */}
|
||||
<div className="flex-1 min-h-0">
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className="h-full overflow-y-auto custom-scrollbar"
|
||||
className="h-full overflow-y-auto overflow-x-hidden custom-scrollbar"
|
||||
style={{
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: 'rgba(255,255,255,0.1) transparent'
|
||||
@ -203,39 +335,51 @@ export default function CreateToVideo2() {
|
||||
>
|
||||
{isLoading && episodeList.length === 0 ? (
|
||||
/* 优化的加载状态 */
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6 pb-6">
|
||||
{[...Array(10)].map((_, index) => (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 pb-6">
|
||||
{[...Array(6)].map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-white/[0.05] backdrop-blur-[20px] border border-white/[0.08] rounded-2xl overflow-hidden animate-pulse"
|
||||
className="group relative aspect-video bg-black/20 rounded-lg overflow-hidden animate-pulse"
|
||||
>
|
||||
<div className="h-[200px] bg-gradient-to-br from-white/[0.08] to-white/[0.04]">
|
||||
<div className="h-full bg-white/[0.06] animate-pulse"></div>
|
||||
</div>
|
||||
<div className="p-5">
|
||||
<div className="h-4 bg-white/[0.08] rounded-lg mb-3 animate-pulse"></div>
|
||||
<div className="h-3 bg-white/[0.06] rounded-lg mb-4 w-3/4 animate-pulse"></div>
|
||||
<div className="flex justify-between">
|
||||
<div className="h-3 bg-white/[0.06] rounded-lg w-20 animate-pulse"></div>
|
||||
<div className="h-3 bg-white/[0.06] rounded-lg w-16 animate-pulse"></div>
|
||||
{/* 背景占位 */}
|
||||
<div className="w-full h-full bg-gradient-to-br from-white/[0.04] to-white/[0.02]" />
|
||||
|
||||
{/* 渐变遮罩 */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent" />
|
||||
|
||||
{/* 状态标签占位 */}
|
||||
<div className="absolute top-3 left-3">
|
||||
<div className="flex items-center gap-2 rounded-full bg-white/10 px-3 py-1">
|
||||
<div className="w-2 h-2 rounded-full bg-white/20"></div>
|
||||
<div className="w-16 h-3 bg-white/20 rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 项目ID占位 */}
|
||||
<div className="absolute top-3 right-3">
|
||||
<div className="w-20 h-3 bg-white/10 rounded-full"></div>
|
||||
</div>
|
||||
|
||||
{/* 底部信息占位 */}
|
||||
<div className="absolute bottom-0 left-0 right-0 p-4">
|
||||
<div className="w-2/3 h-5 bg-white/10 rounded-lg"></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : episodeList.length > 0 ? (
|
||||
/* 优化的剧集网格 */
|
||||
<div className="pb-8">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6">
|
||||
{episodeList.map(renderEpisodeCard)}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{episodeList.map(renderProjectCard)}
|
||||
</div>
|
||||
|
||||
{/* 加载更多指示器 */}
|
||||
{isLoadingMore && (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="flex items-center gap-3 px-6 py-3 bg-white/[0.05] backdrop-blur-[20px] border border-white/[0.08] rounded-xl">
|
||||
<Loader2 className="w-5 h-5 animate-spin text-blue-400" />
|
||||
<span className="text-white/70 font-medium">Loading more episodes...</span>
|
||||
<div className="flex items-center gap-3 px-6 py-3 bg-black/30 backdrop-blur-xl border border-white/10 rounded-full">
|
||||
<Loader2 className="w-5 h-5 animate-spin text-purple-400" />
|
||||
<span className="text-white/90 font-medium">正在加载更多项目...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -244,10 +388,10 @@ export default function CreateToVideo2() {
|
||||
{!hasMore && episodeList.length > 0 && (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 bg-white/[0.05] backdrop-blur-[20px] border border-white/[0.08] rounded-xl flex items-center justify-center mx-auto mb-3">
|
||||
<Check className="w-6 h-6 text-green-400" />
|
||||
<div className="w-12 h-12 bg-black/30 backdrop-blur-xl border border-white/10 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||
<Check className="w-6 h-6 text-purple-400" />
|
||||
</div>
|
||||
<p className="text-white/50 text-sm">All episodes loaded</p>
|
||||
<p className="text-white/70 text-sm">已加载全部项目</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -260,13 +404,9 @@ export default function CreateToVideo2() {
|
||||
</div>
|
||||
|
||||
{/* 视频工具组件 - 使用独立组件 */}
|
||||
<ChatInputBox />
|
||||
|
||||
{episodeList.length === 0 && !isLoading && (
|
||||
<div className='h-full flex flex-col items-center fixed top-[4rem] left-0 right-0 bottom-0'>
|
||||
<EmptyStateAnimation className='' />
|
||||
</div>
|
||||
)}
|
||||
{!isLoading &&
|
||||
<ChatInputBox noData={episodeList.length === 0} />
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -218,8 +218,8 @@ function HomeModule3() {
|
||||
</h2>
|
||||
<p className="text-white text-[1.125rem] leading-[140%] font-normal text-center">
|
||||
MovieFlow can make any kind of film in high quality for you
|
||||
</p>
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
{/* 3x3网格布局 */}
|
||||
<div
|
||||
data-alt="vertical-grid-shadow"
|
||||
@ -293,14 +293,14 @@ function HomeModule3() {
|
||||
videoElement.play();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
/**电影制作工序介绍 */
|
||||
@ -350,7 +350,7 @@ function HomeModule4() {
|
||||
<h2 className="text-white text-[3.375rem] leading-[100%] font-normal ">
|
||||
Edit like you think
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full px-[4rem] gap-[2rem]">
|
||||
{/* 左侧四个切换tab */}
|
||||
@ -518,7 +518,7 @@ function HomeModule5() {
|
||||
Yearly - <span className="text-[#FFCC6D]">10%</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 主要价格卡片 */}
|
||||
<div className="flex justify-between w-[90%] px-[12rem] mb-[2rem]">
|
||||
|
||||
@ -8,11 +8,13 @@ import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { signInWithGoogle, loginUser } from '@/lib/auth';
|
||||
import { GradientText } from '@/components/ui/gradient-text';
|
||||
import { Eye, EyeOff } from 'lucide-react';
|
||||
|
||||
export default function Login() {
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [formError, setFormError] = useState('');
|
||||
const [successMessage, setSuccessMessage] = useState('');
|
||||
@ -117,14 +119,24 @@ export default function Login() {
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">密码</label>
|
||||
<input
|
||||
placeholder="请输入密码"
|
||||
required
|
||||
className="form-control"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<div className="relative">
|
||||
<input
|
||||
placeholder="请输入密码"
|
||||
required
|
||||
className="form-control pr-10"
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
data-alt="toggle-password-visibility"
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-300 focus:outline-none"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{!showPassword ? <EyeOff size={20} /> : <Eye size={20} />}
|
||||
</button>
|
||||
</div>
|
||||
<div className="d-flex justify-content-end mt-1">
|
||||
<a className="auth-link small" href="/forgot-password" data-discover="true">忘记密码?</a>
|
||||
</div>
|
||||
|
||||
@ -422,9 +422,11 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
|
||||
// 渲染视频内容
|
||||
const renderVideoContent = () => {
|
||||
const urls = taskObject.videos.data[currentSketchIndex].urls ? taskObject.videos.data[currentSketchIndex].urls.join(',') : '';
|
||||
return (
|
||||
<div
|
||||
className="relative w-full h-full rounded-lg group"
|
||||
key={`render-video-${urls}`}
|
||||
>
|
||||
{/* 背景模糊的图片 */}
|
||||
{taskObject.videos.data[currentSketchIndex].video_status !== 1 && (
|
||||
@ -551,6 +553,7 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
return (
|
||||
<div
|
||||
className="relative w-full h-full rounded-lg group"
|
||||
key={`render-sketch-${currentSketch.url}`}
|
||||
>
|
||||
{/* 状态 */}
|
||||
{currentSketch.status === 0 && (
|
||||
|
||||
@ -158,10 +158,11 @@ export function ThumbnailGrid({
|
||||
// 渲染视频阶段的缩略图
|
||||
const renderVideoThumbnails = () => (
|
||||
taskObject.videos.data.map((video, index) => {
|
||||
const urls: string = video.urls ? video.urls.join(',') : '';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`video-${index}`}
|
||||
key={`video-${urls}`}
|
||||
className={`relative aspect-video rounded-lg overflow-hidden
|
||||
${currentSketchIndex === index ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}`}
|
||||
onClick={() => !isDragging && onSketchSelect(index)}
|
||||
@ -229,7 +230,7 @@ export function ThumbnailGrid({
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`sketch-${index}`}
|
||||
key={`sketch-${sketch.url}`}
|
||||
className={`relative aspect-video rounded-lg overflow-hidden
|
||||
${currentSketchIndex === index ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}`}
|
||||
onClick={() => !isDragging && onSketchSelect(index)}
|
||||
|
||||
@ -47,6 +47,19 @@ export interface CreateCheckoutSessionData {
|
||||
|
||||
export type CreateCheckoutSessionResponse = ApiResponse<CreateCheckoutSessionData>;
|
||||
|
||||
export interface CreatePortalSessionRequest {
|
||||
user_id: string;
|
||||
return_url?: string;
|
||||
}
|
||||
|
||||
export interface CreatePortalSessionData {
|
||||
portal_url: string;
|
||||
session_id: string;
|
||||
customer_id: string;
|
||||
}
|
||||
|
||||
export type CreatePortalSessionResponse = ApiResponse<CreatePortalSessionData>;
|
||||
|
||||
/**
|
||||
* 获取订阅计划列表
|
||||
* 从后端API获取所有活跃的订阅计划,后端已经过滤了活跃计划
|
||||
@ -104,6 +117,28 @@ export async function getCheckoutSessionStatus(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Customer Portal Session
|
||||
*
|
||||
* 用户可以在Customer Portal中:
|
||||
* 1. 查看当前订阅状态
|
||||
* 2. 更新付款方式
|
||||
* 3. 下载发票和收据
|
||||
* 4. 更改订阅计划
|
||||
* 5. 取消订阅
|
||||
* 6. 查看账单历史
|
||||
*/
|
||||
export async function createPortalSession(
|
||||
request: CreatePortalSessionRequest
|
||||
): Promise<CreatePortalSessionResponse> {
|
||||
try {
|
||||
return await post<CreatePortalSessionResponse>('/api/payment/portal-session', request);
|
||||
} catch (error) {
|
||||
console.error('创建Customer Portal Session失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单的跳转到Checkout页面的工具函数
|
||||
*/
|
||||
@ -112,3 +147,12 @@ export function redirectToCheckout(checkoutUrl: string) {
|
||||
window.open(checkoutUrl, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单的跳转到Customer Portal页面的工具函数
|
||||
*/
|
||||
export function redirectToPortal(portalUrl: string) {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = portalUrl;
|
||||
}
|
||||
}
|
||||
|
||||
BIN
public/assets/cover_image1.jpg
Normal file
BIN
public/assets/cover_image1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 128 KiB |
BIN
public/assets/cover_image2.jpg
Normal file
BIN
public/assets/cover_image2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 103 KiB |
Loading…
x
Reference in New Issue
Block a user