diff --git a/app/globals.css b/app/globals.css
index f8f96bd..29fa569 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -50,7 +50,7 @@
*,
*:after,
*:before {
- box-sizing: border-box;
+ box-sizing: border-box;
}
:root {
@@ -93,10 +93,12 @@
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
-
+
/* 自定义渐变色变量 */
- --custom-blue: 186 100% 70%; /* rgb(106, 244, 249) */
- --custom-purple: 280 100% 62%; /* rgb(199, 59, 255) */
+ --custom-blue: 186 100% 70%;
+ /* rgb(106, 244, 249) */
+ --custom-purple: 280 100% 62%;
+ /* rgb(199, 59, 255) */
--custom-blue-rgb: 106, 244, 249;
--custom-purple-rgb: 199, 59, 255;
--destructive: 0 62.8% 30.6%;
@@ -162,6 +164,7 @@ body {
.hide-scrollbar::-webkit-scrollbar {
display: none !important;
}
+
*::-webkit-scrollbar {
display: none !important;
}
@@ -184,6 +187,7 @@ body {
border-radius: 8px;
padding: 8px;
}
+
.button-NxtqWZ:hover {
background-color: #2f3237 !important;
}
@@ -226,6 +230,7 @@ body {
height: 0;
pointer-events: none;
}
+
.ant-spin-nested-loading .ant-spin {
max-height: none !important;
}
@@ -236,6 +241,7 @@ body {
opacity: 0;
transform: translateY(-8px);
}
+
to {
opacity: 1;
transform: translateY(0);
@@ -256,9 +262,47 @@ body {
.ant-switch.ant-switch-checked:hover {
background: rgb(146 78 173) !important;
-}
+}
.language-dropdown li {
padding: unset !important;
margin: 0.25rem !important;
+}
+
+/* 安全区域变量定义 */
+:root {
+ --sat: env(safe-area-inset-top, 0px);
+ --sar: env(safe-area-inset-right, 0px);
+ --sab: env(safe-area-inset-bottom, 0px);
+ --sal: env(safe-area-inset-left, 0px);
+}
+
+/* 移动端适配:使用 dvh 动态视口高度 */
+@supports (height: 100dvh) {
+ body {
+ height: 100dvh;
+ }
+}
+
+/* 移动端安全区域处理 */
+@media (max-width: 768px) {
+ body {
+ /* 使用动态视口高度,考虑移动端浏览器地址栏 */
+ height: 100dvh;
+ height: calc(var(--vh, 1vh) * 100);
+ padding-bottom: env(safe-area-inset-bottom);
+ }
+}
+
+/* 针对移动端底部导航栏/状态栏的特殊处理 */
+@media (max-width: 768px) and (display-mode: browser) {
+ .mobile-safe-bottom {
+ padding-bottom: max(1rem, env(safe-area-inset-bottom));
+ margin-bottom: max(1rem, env(safe-area-inset-bottom));
+ }
+
+ .mobile-viewport-height {
+ height: 100dvh;
+ height: calc(var(--vh, 1vh) * 100);
+ }
}
\ No newline at end of file
diff --git a/app/layout.tsx b/app/layout.tsx
index db5e91e..118ff61 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -43,7 +43,7 @@ export default function RootLayout({
MovieFlow - AI Movie Studio
-
+
diff --git a/app/pay-redirect/page.tsx b/app/pay-redirect/page.tsx
index 24fc220..f11b8b5 100644
--- a/app/pay-redirect/page.tsx
+++ b/app/pay-redirect/page.tsx
@@ -2,38 +2,75 @@
import React from "react";
import { TailwindSpinner } from "@/components/common/GlobalLoad";
+import { useSearchParams } from "next/navigation";
+import { createCheckoutSession, buyTokens } from "@/lib/stripe";
export default function PayRedirectPage() {
- const [status, setStatus] = React.useState("Waiting for secure checkout redirect...");
+ const [status, setStatus] = React.useState("Preparing checkout...");
const [error, setError] = React.useState("");
+ const searchParams = useSearchParams();
React.useEffect(() => {
- const handleMessage = (event: MessageEvent) => {
+ const type = (searchParams.get("type") || "").toLowerCase();
+ if (!type) {
+ setStatus("");
+ setError("Missing payment type.");
+ return;
+ }
+
+ const redirectTo = async () => {
try {
- if (event.origin !== window.location.origin) return;
- const data = event.data || {};
- if (data?.type === "redirect-to-payment" && typeof data?.url === "string") {
- setStatus("Redirecting to Stripe Checkout...");
- window.location.href = data.url as string;
- } else if (data?.type === "redirect-error") {
- setError(typeof data?.message === "string" ? data.message : "Failed to create payment. Please close this page and try again.");
+ if (type === "token") {
+ const amountStr = searchParams.get("amount");
+ const pkg = searchParams.get("pkg") || "basic";
+ const amount = Number(amountStr);
+ if (!amount || amount <= 0) {
+ throw new Error("Invalid token amount");
+ }
+ setStatus("Creating token purchase session...");
+ const resp = await buyTokens({ token_amount: amount, package_type: pkg });
+ if (resp?.successful && resp?.data?.checkout_url) {
+ setStatus("Redirecting to Stripe Checkout...");
+ window.location.href = resp.data.checkout_url as string;
+ return;
+ }
+ throw new Error(resp?.message || "Failed to create token checkout session");
}
- } catch {
- setError("An error occurred while processing redirect info. Please close this page and try again.");
+
+ if (type === "subscription") {
+ const plan = searchParams.get("plan");
+ const billing = (searchParams.get("billing") || "month") as "month" | "year";
+ if (!plan) {
+ throw new Error("Missing plan name");
+ }
+ const currentUser = JSON.parse(localStorage.getItem("currentUser") || "{}");
+ if (!currentUser?.id) {
+ throw new Error("Not logged in. Please sign in and try again.");
+ }
+ setStatus("Creating subscription session...");
+ const result = await createCheckoutSession({
+ user_id: String(currentUser.id),
+ plan_name: plan,
+ billing_cycle: billing,
+ });
+ if (result?.successful && result?.data?.checkout_url) {
+ setStatus("Redirecting to Stripe Checkout...");
+ window.location.href = result.data.checkout_url as string;
+ return;
+ }
+ throw new Error(result?.message || "Failed to create subscription session");
+ }
+
+ throw new Error("Unsupported payment type");
+ } catch (e: unknown) {
+ setStatus("");
+ setError(e instanceof Error ? e.message : "Failed to prepare checkout.");
}
};
- window.addEventListener("message", handleMessage);
- const timeoutId = window.setTimeout(() => {
- setStatus("");
- setError("No redirect instruction received. It may be a network issue or the page was blocked. Please go back and try again.");
- }, 15000);
-
- return () => {
- window.removeEventListener("message", handleMessage);
- window.clearTimeout(timeoutId);
- };
- }, []);
+ redirectTo();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [searchParams]);
return (
diff --git a/app/pricing/page.tsx b/app/pricing/page.tsx
index 9ea40bd..01291e4 100644
--- a/app/pricing/page.tsx
+++ b/app/pricing/page.tsx
@@ -85,54 +85,18 @@ function HomeModule5() {
const handleSubscribe = async (planName: string) => {
setLoadingPlan(planName);
-
- // 先同步打开同域重定向页,避免拦截
- const redirectWindow = window.open('/pay-redirect', '_blank');
- if (!redirectWindow) {
+ // 改为直接携带参数打开 pay-redirect,由其内部完成创建与跳转
+ const url = `/pay-redirect?type=subscription&plan=${encodeURIComponent(planName)}&billing=${encodeURIComponent(billingType)}`;
+ const win = window.open(url, '_blank');
+ // 通知当前窗口等待支付(显示loading模态框)
+ window.postMessage({
+ type: 'waiting-payment',
+ paymentType: 'subscription',
+ }, '*');
+ if (!win) {
setLoadingPlan(null);
throw new Error('Unable to open redirect window, please check popup settings');
}
-
- try {
- const { createCheckoutSession } = await import("@/lib/stripe");
-
- const User = JSON.parse(localStorage.getItem("currentUser") || "{}");
- if (!User.id) {
- throw new Error("Unable to obtain user ID, please log in again");
- }
-
- const result = await createCheckoutSession({
- user_id: String(User.id),
- plan_name: planName,
- billing_cycle: billingType,
- });
-
- if (!result.successful || !result.data?.checkout_url) {
- throw new Error("create checkout session failed");
- }
-
- // 通知当前窗口等待支付(显示loading模态框)
- window.postMessage({
- type: 'waiting-payment',
- paymentType: 'subscription',
- }, '*');
-
- // 通过 postMessage 通知新页面执行重定向
- redirectWindow.postMessage({
- type: 'redirect-to-payment',
- url: result.data.checkout_url,
- }, window.location.origin);
- } catch (error) {
- // 通知新页错误信息
- try {
- redirectWindow.postMessage({
- type: 'redirect-error',
- message: 'Failed to create checkout session',
- }, window.location.origin);
- } catch {}
- setLoadingPlan(null);
- throw new Error("create checkout session failed, please try again later");
- }
};
return (
+
{/* Header */}
diff --git a/components/layout/dashboard-layout.tsx b/components/layout/dashboard-layout.tsx
index 6008f28..0d77dd4 100644
--- a/components/layout/dashboard-layout.tsx
+++ b/components/layout/dashboard-layout.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
import { Sidebar } from './sidebar';
import { TopBar } from './top-bar';
import { useDeviceType } from '@/hooks/useDeviceType';
@@ -10,9 +10,28 @@ interface DashboardLayoutProps {
}
export function DashboardLayout({ children }: DashboardLayoutProps) {
- const [sidebarCollapsed, setSidebarCollapsed] = useState(true); // 默认收起状态
+ const [sidebarCollapsed, setSidebarCollapsed] = useState(true);
const { deviceType, isMobile, isTablet, isDesktop } = useDeviceType();
+ // 处理移动端视口高度动态计算
+ useEffect(() => {
+ if (isMobile || isTablet) {
+ const setVH = () => {
+ const vh = window.innerHeight * 0.01;
+ document.documentElement.style.setProperty('--vh', `${vh}px`);
+ };
+
+ setVH();
+ window.addEventListener('resize', setVH);
+ window.addEventListener('orientationchange', setVH);
+
+ return () => {
+ window.removeEventListener('resize', setVH);
+ window.removeEventListener('orientationchange', setVH);
+ };
+ }
+ }, [isMobile, isTablet]);
+
// 根据设备类型设置布局样式
const getLayoutStyles = () => {
if (isMobile || isTablet) {
@@ -29,13 +48,28 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
};
};
+ // 获取移动端容器类名
+ const getMobileContainerClasses = () => {
+ if (isMobile || isTablet) {
+ return "mobile-viewport-height mobile-safe-bottom";
+ }
+ return "";
+ };
+
return (
-
+
{isDesktop &&
}
+ className={`top-[4rem] fixed right-0 bottom-0 px-4 ${getMobileContainerClasses()}`}
+ style={{
+ ...getLayoutStyles(),
+ // 移动端使用动态高度计算
+ height: (isMobile || isTablet)
+ ? 'calc(100dvh - 4rem)'
+ : 'calc(100vh - 4rem)'
+ }}
+ >
{children}
diff --git a/components/layout/top-bar.tsx b/components/layout/top-bar.tsx
index 365fab3..92d6839 100644
--- a/components/layout/top-bar.tsx
+++ b/components/layout/top-bar.tsx
@@ -88,7 +88,7 @@ export function TopBar({ collapsed, isDesktop=true }: { collapsed: boolean, isDe
}
};
- // 处理Token购买(同域新页 + postMessage 方式重定向)
+ // 处理Token购买(改为携带参数打开 pay-redirect)
const handleBuyTokens = async (tokenAmount: number) => {
if (!currentUser?.id) {
console.error("用户未登录");
@@ -99,52 +99,14 @@ export function TopBar({ collapsed, isDesktop=true }: { collapsed: boolean, isDe
console.error("Token数量必须大于0");
return;
}
-
- // 先同步打开同域新页面,避免被拦截
- const redirectWindow = window.open("/pay-redirect", "_blank");
- if (!redirectWindow) {
- console.error("无法打开支付重定向页面,可能被浏览器拦截");
- return;
- }
-
- setIsBuyingTokens(true);
- try {
- const response = await buyTokens({
- token_amount: tokenAmount,
- package_type: "basic"
- });
-
- if (response.successful && response.data?.checkout_url) {
- // 通知当前窗口等待支付,标识为Token购买
- window.postMessage({
- type: "waiting-payment",
- paymentType: "token"
- }, "*");
- sessionStorage.setItem('session_id', response.data.session_id);
- // 通过 postMessage 通知新页面进行重定向
- redirectWindow.postMessage({
- type: "redirect-to-payment",
- url: response.data.checkout_url
- }, window.location.origin);
- } else {
- console.error("创建Token购买失败:", response.message);
- // 通知新页显示错误
- redirectWindow.postMessage({
- type: "redirect-error",
- message: response.message || "创建支付失败"
- }, window.location.origin);
- }
- } catch (error: unknown) {
- console.error("Token购买失败:", error);
- try {
- redirectWindow.postMessage({
- type: "redirect-error",
- message: "网络或服务异常,请关闭此页重试"
- }, window.location.origin);
- } catch {}
- } finally {
- setIsBuyingTokens(false);
- }
+ // 直接打开带参数的 pay-redirect,新窗口内自行创建会话并跳转
+ const url = `/pay-redirect?type=token&amount=${encodeURIComponent(tokenAmount)}&pkg=basic`;
+ window.open(url, "_blank");
+ // 通知当前窗口等待支付(显示loading模态框)
+ window.postMessage({
+ type: 'waiting-payment',
+ paymentType: 'subscription',
+ }, '*');
};
// 处理自定义金额购买
@@ -255,7 +217,7 @@ export function TopBar({ collapsed, isDesktop=true }: { collapsed: boolean, isDe
return (
window.removeEventListener('home-menu-toggle' as any, handler as any);
}, []);
return (
-
+
{/* 桌面端菜单(居中,仅三个项) */}
@@ -1249,34 +1249,16 @@ function HomeModule5() {
const handleSubscribe = async (planName: string) => {
localStorage.setItem("callBackUrl", pathname);
- try {
- // 使用新的Checkout Session方案(更简单!)
- const { createCheckoutSession, redirectToCheckout } = await import(
- "@/lib/stripe"
- );
-
- // 从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: billingType,
- });
-
- if (!result.successful || !result.data) {
- throw new Error("create checkout session failed");
- }
-
- setShowCallbackModal(true);
- window.open(result.data.checkout_url, "_blank");
- } catch (error) {
- throw new Error("create checkout session failed, please try again later");
+ // 改为直接携带参数打开 pay-redirect,由其内部完成创建与跳转
+ const url = `/pay-redirect?type=subscription&plan=${encodeURIComponent(planName)}&billing=${encodeURIComponent(billingType)}`;
+ const win = window.open(url, "_blank");
+ // 通知当前窗口等待支付(显示loading模态框)
+ window.postMessage({
+ type: 'waiting-payment',
+ paymentType: 'subscription',
+ }, '*');
+ if (!win) {
+ throw new Error("Unable to open redirect window, please check popup settings");
}
};
return (
diff --git a/components/pages/work-flow/H5MediaViewer.tsx b/components/pages/work-flow/H5MediaViewer.tsx
index 6ddcf36..8a4940f 100644
--- a/components/pages/work-flow/H5MediaViewer.tsx
+++ b/components/pages/work-flow/H5MediaViewer.tsx
@@ -301,7 +301,7 @@ export function H5MediaViewer({