diff --git a/app/layout.tsx b/app/layout.tsx index 44acd72..d43e8ed 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -33,8 +33,10 @@ export default function RootLayout({ }} > - - {children} + {/* */} +
+ {children} +
diff --git a/app/payCallback/page.tsx b/app/payCallback/page.tsx new file mode 100644 index 0000000..451c32f --- /dev/null +++ b/app/payCallback/page.tsx @@ -0,0 +1,197 @@ +'use client' + +import React, { useEffect, useState } from 'react' +import { Result, Button, Spin, Card } from 'antd' +import { CheckCircleOutlined, CloseCircleOutlined, LoadingOutlined } from '@ant-design/icons' +import { useRouter, useSearchParams } from 'next/navigation' +import { getCheckoutSessionStatus, PaymentStatusResponse } from '@/lib/stripe' + +/** + * 支付状态枚举 + */ +enum PaymentStatus { + LOADING = 'loading', + SUCCESS = 'success', + FAILED = 'failed' +} + +/** + * 支付回调页面组件 + * 处理Stripe Checkout支付完成后的状态展示和用户操作 + */ +export default function PayCallbackPage() { + const router = useRouter() + const searchParams = useSearchParams() + const [paymentStatus, setPaymentStatus] = useState(PaymentStatus.LOADING) + const [paymentInfo, setPaymentInfo] = useState(null) + const callBackUrl = localStorage.getItem('callBackUrl') || '/' + + /** + * 获取Stripe Checkout Session支付状态 + */ + const fetchPaymentStatus = async () => { + try { + // 从URL参数获取session_id和user_id + const sessionId = searchParams.get('session_id') + const userId = searchParams.get('user_id') + + if (!sessionId || !userId) { + throw new Error('缺少必要的参数: session_id 或 user_id') + } + + // 调用真实的Stripe API获取支付状态 + const response = await getCheckoutSessionStatus(sessionId, userId) + + if (response.successful && response.data) { + const { payment_status, biz_order_no, pay_time, subscription } = response.data + + if (payment_status === 'success') { + setPaymentStatus(PaymentStatus.SUCCESS) + setPaymentInfo({ + orderId: biz_order_no, + sessionId, + paymentTime: pay_time, + subscription: subscription ? { + planName: subscription.plan_name, + planDisplayName: subscription.plan_display_name, + status: subscription.status, + currentPeriodEnd: subscription.current_period_end + } : null + }) + } else if (payment_status === 'fail') { + setPaymentStatus(PaymentStatus.FAILED) + setPaymentInfo({ + orderId: biz_order_no, + sessionId, + errorMessage: '支付处理失败,请重试' + }) + } else { + // pending状态,继续等待 + setTimeout(fetchPaymentStatus, 2000) + } + } else { + throw new Error(response.message || '获取支付状态失败') + } + } catch (error) { + console.error('获取支付状态失败:', error) + setPaymentStatus(PaymentStatus.FAILED) + setPaymentInfo({ + errorMessage: error instanceof Error ? error.message : '网络错误,无法获取支付状态' + }) + } + } + + /** + * 返回上一页的函数 + */ + const handleGoBack = () => { + router.push(callBackUrl) + } + + /** + * 重新支付的函数 + */ + const handleRetryPayment = () => { + router.push('/pricing') + } + + useEffect(() => { + fetchPaymentStatus() + }, []) + + // 加载状态 + if (paymentStatus === PaymentStatus.LOADING) { + return ( +
+
+ } + size="large" + /> +
正在获取支付状态...
+
请稍候,我们正在处理您的支付信息
+
+
+ ) + } + + // 支付成功状态 + if (paymentStatus === PaymentStatus.SUCCESS) { + return ( +
+
+
+ +

支付成功!

+

{`订单号: ${paymentInfo?.orderId || 'N/A'}`}

+
+ +
+ {paymentInfo?.subscription && ( + <> +

订阅计划: {paymentInfo.subscription.planDisplayName}

+

订阅状态: {paymentInfo.subscription.status}

+ {paymentInfo.subscription.currentPeriodEnd && ( +

到期时间: {new Date(paymentInfo.subscription.currentPeriodEnd).toLocaleString()}

+ )} + + )} + {paymentInfo?.paymentTime && ( +

支付时间: {new Date(paymentInfo.paymentTime).toLocaleString()}

+ )} +

感谢您的购买!

+
+ + +
+
+ ) + } + + // 支付失败状态 + return ( +
+
+
+ +

支付失败

+

{paymentInfo?.errorMessage || '支付处理过程中发生错误'}

+
+ +
+

订单号: {paymentInfo?.orderId || 'N/A'}

+

如果问题持续存在,请联系客服

+
+ +
+ + + +
+
+
+ ) +} diff --git a/app/pricing/page.tsx b/app/pricing/page.tsx index 7f61a3a..8c0f6da 100644 --- a/app/pricing/page.tsx +++ b/app/pricing/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { useRouter } from 'next/navigation'; import { Check, ArrowLeft } from 'lucide-react'; import { Button } from '@/components/ui/button'; @@ -8,8 +8,20 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { fetchSubscriptionPlans, SubscriptionPlan } from '@/lib/stripe'; export default function PricingPage() { - const router = useRouter(); - const [billingCycle, setBillingCycle] = useState<'month' | 'year'>('month'); + + return ( +
+ {/* Main Content */} + +
+ ); +} +/**价格方案 */ +function HomeModule5() { + const [billingType, setBillingType] = useState<'month' | 'year'>( + "month" + ); + const [plans, setPlans] = useState([]); // 从后端获取订阅计划数据 @@ -26,25 +38,23 @@ export default function PricingPage() { loadPlans(); }, []); - // 转换后端数据为前端显示格式,保持原有的数据结构 - const transformPlanForDisplay = (plan: SubscriptionPlan) => { - const monthlyPrice = plan.price_month / 100; // 后端存储的是分,转换为元 - const yearlyPrice = plan.price_year / 100; - - return { - name: plan.name, - displayName: plan.display_name, - price: { - month: monthlyPrice, - year: yearlyPrice - }, - description: plan.description, - features: plan.features || [], - popular: plan.is_popular, // 使用后端返回的 is_popular 字段 - buttonText: plan.is_free ? 'Start Free Trial' : 'Subscribe', - buttonVariant: plan.is_free ? 'outline' as const : 'default' as const - }; - }; + const pricingPlans = useMemo<{ + title: string; + price: number; + credits: string; + buttonText: string; + features: string[]; + }[]>(() => { + return plans.map((plan) => { + return { + title: plan.display_name || plan.name, + price: billingType === "month" ? plan.price_month/100 : plan.price_year/100, + credits: plan.description, + buttonText: plan.is_free ? "Try For Free" : "Subscribe Now", + features: plan.features || [], + }; + }); + }, [plans, billingType]); const handleSubscribe = async (planName: string) => { if (planName === 'hobby') { @@ -57,226 +67,145 @@ export default function PricingPage() { // 从localStorage获取当前用户信息 const User = JSON.parse(localStorage.getItem("currentUser") || "{}"); - + if (!User.id) { throw new Error("无法获取用户ID,请重新登录"); } - + // 1. 创建Checkout Session const result = await createCheckoutSession({ user_id: String(User.id), plan_name: planName, - billing_cycle: billingCycle + billing_cycle: billingType }); - + if (!result.successful || !result.data) { throw new Error("create checkout session failed"); } // 2. 直接跳转到Stripe托管页面(就这么简单!) redirectToCheckout(result.data.checkout_url); - + } catch (error) { throw new Error("create checkout session failed, please try again later"); } }; - - // 如果还没有加载到数据,显示加载状态但保持原有样式 - if (plans.length === 0) { - return ( -
-
-
- -
- -
-

Pricing

-

Choose the plan that suits you best

- -
- - -
-
- -
-
-

正在加载订阅计划...

-
-
-
-
- ); - } - return ( -
- {/* Main Content */} -
- {/* Back Button */} -
- + Monthly + +
- -
-

Pricing

-

Choose the plan that suits you best

- - {/* Billing Toggle */} -
-
+ + {/* 主要价格卡片 */} +
+ {pricingPlans.map((plan, index) => ( +
+

+ {plan.title} +

+
+ + ${plan.price} + /month +
+

+ {plan.credits} +

+ - +

+ * Billed monthly until cancelled +

+
    + {plan.features.map((feature, featureIndex) => ( +
  • + + {feature} +
  • + ))} +
+ ))} +
+ + {/* 额外价格卡片 */} +
+
+

+ Free +

+
+ $0 +
+

+ 10 Video mins and 1 AI credit per week, 1 Express avatar, 4 Exports + per week with invideo watermark. +

+

+ No access to generative features. +

+
- {/* Plans Section */} -
-
- {plans.map((plan) => { - const displayPlan = transformPlanForDisplay(plan); - - return ( -
- {/* 预留标签空间,确保所有卡片组合高度一致 */} -
- {displayPlan.popular && ( -
- Most Popular -
- )} -
- - - - - {displayPlan.displayName} - -
-
- {displayPlan.price[billingCycle] === 0 ? ( - 'Free' - ) : ( - <> - ${displayPlan.price[billingCycle]} - - /{billingCycle === 'month' ? 'month' : 'year'} - - - )} -
-
- - - {displayPlan.description} - -
- - -
- {displayPlan.features.map((feature, index) => ( -
-
- -
- {feature} -
- ))} -
- - -
-
-
- ); - })} -
+
+

+ Enterprise +

+

Custom

+

+ Custom solutions for large organizations. Advanced security and + flexible pricing based on your needs. +

+

+ on your needs. +

+
-
+
); } diff --git a/components/ChatInputBox/ChatInputBox.tsx b/components/ChatInputBox/ChatInputBox.tsx index 76ef8d1..e7d64d0 100644 --- a/components/ChatInputBox/ChatInputBox.tsx +++ b/components/ChatInputBox/ChatInputBox.tsx @@ -173,8 +173,8 @@ const RenderTemplateStoryMode = ({ // 模板列表渲染 const templateListRender = () => { return ( -
-
+
+
{templateStoryList.map((template, index) => (
-
+
{/* 弹窗头部 */}

@@ -576,7 +576,7 @@ const RenderTemplateStoryMode = ({

-
+
{templateListRender()}
{storyEditorRender()}
@@ -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 ( -
+
{/* 视频故事板工具面板 - 毛玻璃效果背景 */}
{/* 展开/收起控制区域 */} - {isExpanded ? ( - // 展开状态:显示收起按钮和提示 -
setIsExpanded(false)} - > - - Click to action -
- ) : ( - // 收起状态:显示展开按钮 -
setIsExpanded(true)} - > - -
- )} + { + !noData && ( + <> + {isExpanded ? ( + // 展开状态:显示收起按钮和提示 +
setIsExpanded(false)} + > + + Click to action +
+ ) : ( + // 收起状态:显示展开按钮 +
setIsExpanded(true)} + > + +
+ )} + + ) + } {/* 主要内容区域 - 简化层级,垂直居中 */}
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; diff --git a/components/QueueBox/QueueNotification.tsx b/components/QueueBox/QueueNotification.tsx new file mode 100644 index 0000000..e296513 --- /dev/null +++ b/components/QueueBox/QueueNotification.tsx @@ -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 = () => ( +
+ + {/* 胶片外框 */} + + {/* 齿孔 */} + + + + + {/* 胶片画面区域 */} + + +
+); + +/** 放映机音效组件 */ +const ProjectorSound = () => ( +