forked from 77media/video-flow
Merge branch 'dev' into prod
This commit is contained in:
commit
2a11235314
@ -50,7 +50,7 @@
|
|||||||
*,
|
*,
|
||||||
*:after,
|
*:after,
|
||||||
*:before {
|
*:before {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
@ -93,10 +93,12 @@
|
|||||||
--muted-foreground: 0 0% 63.9%;
|
--muted-foreground: 0 0% 63.9%;
|
||||||
--accent: 0 0% 14.9%;
|
--accent: 0 0% 14.9%;
|
||||||
--accent-foreground: 0 0% 98%;
|
--accent-foreground: 0 0% 98%;
|
||||||
|
|
||||||
/* 自定义渐变色变量 */
|
/* 自定义渐变色变量 */
|
||||||
--custom-blue: 186 100% 70%; /* rgb(106, 244, 249) */
|
--custom-blue: 186 100% 70%;
|
||||||
--custom-purple: 280 100% 62%; /* rgb(199, 59, 255) */
|
/* rgb(106, 244, 249) */
|
||||||
|
--custom-purple: 280 100% 62%;
|
||||||
|
/* rgb(199, 59, 255) */
|
||||||
--custom-blue-rgb: 106, 244, 249;
|
--custom-blue-rgb: 106, 244, 249;
|
||||||
--custom-purple-rgb: 199, 59, 255;
|
--custom-purple-rgb: 199, 59, 255;
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 62.8% 30.6%;
|
||||||
@ -162,6 +164,7 @@ body {
|
|||||||
.hide-scrollbar::-webkit-scrollbar {
|
.hide-scrollbar::-webkit-scrollbar {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar {
|
*::-webkit-scrollbar {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
@ -184,6 +187,7 @@ body {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-NxtqWZ:hover {
|
.button-NxtqWZ:hover {
|
||||||
background-color: #2f3237 !important;
|
background-color: #2f3237 !important;
|
||||||
}
|
}
|
||||||
@ -226,6 +230,7 @@ body {
|
|||||||
height: 0;
|
height: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-spin-nested-loading .ant-spin {
|
.ant-spin-nested-loading .ant-spin {
|
||||||
max-height: none !important;
|
max-height: none !important;
|
||||||
}
|
}
|
||||||
@ -236,6 +241,7 @@ body {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-8px);
|
transform: translateY(-8px);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
@ -256,9 +262,47 @@ body {
|
|||||||
|
|
||||||
.ant-switch.ant-switch-checked:hover {
|
.ant-switch.ant-switch-checked:hover {
|
||||||
background: rgb(146 78 173) !important;
|
background: rgb(146 78 173) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.language-dropdown li {
|
.language-dropdown li {
|
||||||
padding: unset !important;
|
padding: unset !important;
|
||||||
margin: 0.25rem !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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -43,7 +43,7 @@ export default function RootLayout({
|
|||||||
<head>
|
<head>
|
||||||
<title>MovieFlow - AI Movie 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="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="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||||
<link rel="icon" type="image/x-icon" sizes="16x16" href="/favicon.ico?v=1" />
|
<link rel="icon" type="image/x-icon" sizes="16x16" href="/favicon.ico?v=1" />
|
||||||
<link rel="icon" type="image/x-icon" sizes="32x32" href="/favicon.ico?v=1" />
|
<link rel="icon" type="image/x-icon" sizes="32x32" href="/favicon.ico?v=1" />
|
||||||
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico?v=1" />
|
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico?v=1" />
|
||||||
|
|||||||
@ -2,38 +2,75 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { TailwindSpinner } from "@/components/common/GlobalLoad";
|
import { TailwindSpinner } from "@/components/common/GlobalLoad";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
|
import { createCheckoutSession, buyTokens } from "@/lib/stripe";
|
||||||
|
|
||||||
export default function PayRedirectPage() {
|
export default function PayRedirectPage() {
|
||||||
const [status, setStatus] = React.useState<string>("Waiting for secure checkout redirect...");
|
const [status, setStatus] = React.useState<string>("Preparing checkout...");
|
||||||
const [error, setError] = React.useState<string>("");
|
const [error, setError] = React.useState<string>("");
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const handleMessage = (event: MessageEvent) => {
|
const type = (searchParams.get("type") || "").toLowerCase();
|
||||||
|
if (!type) {
|
||||||
|
setStatus("");
|
||||||
|
setError("Missing payment type.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectTo = async () => {
|
||||||
try {
|
try {
|
||||||
if (event.origin !== window.location.origin) return;
|
if (type === "token") {
|
||||||
const data = event.data || {};
|
const amountStr = searchParams.get("amount");
|
||||||
if (data?.type === "redirect-to-payment" && typeof data?.url === "string") {
|
const pkg = searchParams.get("pkg") || "basic";
|
||||||
setStatus("Redirecting to Stripe Checkout...");
|
const amount = Number(amountStr);
|
||||||
window.location.href = data.url as string;
|
if (!amount || amount <= 0) {
|
||||||
} else if (data?.type === "redirect-error") {
|
throw new Error("Invalid token amount");
|
||||||
setError(typeof data?.message === "string" ? data.message : "Failed to create payment. Please close this page and try again.");
|
}
|
||||||
|
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);
|
redirectTo();
|
||||||
const timeoutId = window.setTimeout(() => {
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
setStatus("");
|
}, [searchParams]);
|
||||||
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);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-alt="pay-redirect-page" className="min-h-screen w-full flex items-center justify-center bg-black text-white">
|
<div data-alt="pay-redirect-page" className="min-h-screen w-full flex items-center justify-center bg-black text-white">
|
||||||
|
|||||||
@ -85,54 +85,18 @@ function HomeModule5() {
|
|||||||
|
|
||||||
const handleSubscribe = async (planName: string) => {
|
const handleSubscribe = async (planName: string) => {
|
||||||
setLoadingPlan(planName);
|
setLoadingPlan(planName);
|
||||||
|
// 改为直接携带参数打开 pay-redirect,由其内部完成创建与跳转
|
||||||
// 先同步打开同域重定向页,避免拦截
|
const url = `/pay-redirect?type=subscription&plan=${encodeURIComponent(planName)}&billing=${encodeURIComponent(billingType)}`;
|
||||||
const redirectWindow = window.open('/pay-redirect', '_blank');
|
const win = window.open(url, '_blank');
|
||||||
if (!redirectWindow) {
|
// 通知当前窗口等待支付(显示loading模态框)
|
||||||
|
window.postMessage({
|
||||||
|
type: 'waiting-payment',
|
||||||
|
paymentType: 'subscription',
|
||||||
|
}, '*');
|
||||||
|
if (!win) {
|
||||||
setLoadingPlan(null);
|
setLoadingPlan(null);
|
||||||
throw new Error('Unable to open redirect window, please check popup settings');
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -169,7 +169,7 @@ export default function SmartChatBox({
|
|||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${isMobile ? 'z-[49]' : 'h-full'} w-full text-gray-100 flex flex-col`} data-alt="smart-chat-box">
|
<div className={`${isMobile ? 'z-[49] relative' : 'h-full'} w-full text-gray-100 flex flex-col`} data-alt="smart-chat-box">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className={`px-4 py-3 border-b border-white/10 flex items-center justify-between ${isMobile ? 'sticky top-0 bg-[#141414] z-[1]' : ''}`} data-alt="chat-header">
|
<div className={`px-4 py-3 border-b border-white/10 flex items-center justify-between ${isMobile ? 'sticky top-0 bg-[#141414] z-[1]' : ''}`} data-alt="chat-header">
|
||||||
<div className="font-semibold flex items-center gap-2">
|
<div className="font-semibold flex items-center gap-2">
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Sidebar } from './sidebar';
|
import { Sidebar } from './sidebar';
|
||||||
import { TopBar } from './top-bar';
|
import { TopBar } from './top-bar';
|
||||||
import { useDeviceType } from '@/hooks/useDeviceType';
|
import { useDeviceType } from '@/hooks/useDeviceType';
|
||||||
@ -10,9 +10,28 @@ interface DashboardLayoutProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function DashboardLayout({ children }: DashboardLayoutProps) {
|
export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(true); // 默认收起状态
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(true);
|
||||||
const { deviceType, isMobile, isTablet, isDesktop } = useDeviceType();
|
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 = () => {
|
const getLayoutStyles = () => {
|
||||||
if (isMobile || isTablet) {
|
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 (
|
return (
|
||||||
<div className=" min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
<TopBar collapsed={sidebarCollapsed} isDesktop={isDesktop} />
|
<TopBar collapsed={sidebarCollapsed} isDesktop={isDesktop} />
|
||||||
{isDesktop && <Sidebar collapsed={sidebarCollapsed} onToggle={setSidebarCollapsed} />}
|
{isDesktop && <Sidebar collapsed={sidebarCollapsed} onToggle={setSidebarCollapsed} />}
|
||||||
<div
|
<div
|
||||||
className="h-[calc(100vh-4rem)] top-[4rem] fixed right-0 bottom-0 px-4"
|
className={`top-[4rem] fixed right-0 bottom-0 px-4 ${getMobileContainerClasses()}`}
|
||||||
style={getLayoutStyles()}>
|
style={{
|
||||||
|
...getLayoutStyles(),
|
||||||
|
// 移动端使用动态高度计算
|
||||||
|
height: (isMobile || isTablet)
|
||||||
|
? 'calc(100dvh - 4rem)'
|
||||||
|
: 'calc(100vh - 4rem)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -88,7 +88,7 @@ export function TopBar({ collapsed, isDesktop=true }: { collapsed: boolean, isDe
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理Token购买(同域新页 + postMessage 方式重定向)
|
// 处理Token购买(改为携带参数打开 pay-redirect)
|
||||||
const handleBuyTokens = async (tokenAmount: number) => {
|
const handleBuyTokens = async (tokenAmount: number) => {
|
||||||
if (!currentUser?.id) {
|
if (!currentUser?.id) {
|
||||||
console.error("用户未登录");
|
console.error("用户未登录");
|
||||||
@ -99,52 +99,14 @@ export function TopBar({ collapsed, isDesktop=true }: { collapsed: boolean, isDe
|
|||||||
console.error("Token数量必须大于0");
|
console.error("Token数量必须大于0");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 直接打开带参数的 pay-redirect,新窗口内自行创建会话并跳转
|
||||||
// 先同步打开同域新页面,避免被拦截
|
const url = `/pay-redirect?type=token&amount=${encodeURIComponent(tokenAmount)}&pkg=basic`;
|
||||||
const redirectWindow = window.open("/pay-redirect", "_blank");
|
window.open(url, "_blank");
|
||||||
if (!redirectWindow) {
|
// 通知当前窗口等待支付(显示loading模态框)
|
||||||
console.error("无法打开支付重定向页面,可能被浏览器拦截");
|
window.postMessage({
|
||||||
return;
|
type: 'waiting-payment',
|
||||||
}
|
paymentType: 'subscription',
|
||||||
|
}, '*');
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理自定义金额购买
|
// 处理自定义金额购买
|
||||||
@ -255,7 +217,7 @@ export function TopBar({ collapsed, isDesktop=true }: { collapsed: boolean, isDe
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="fixed right-0 top-0 h-16 header z-[999]"
|
className="fixed right-0 top-0 h-16 header z-[20]"
|
||||||
style={{
|
style={{
|
||||||
isolation: "isolate",
|
isolation: "isolate",
|
||||||
left: (pathname === "/" || !isDesktop) ? "0" : (collapsed ? "2.5rem" : "16rem")
|
left: (pathname === "/" || !isDesktop) ? "0" : (collapsed ? "2.5rem" : "16rem")
|
||||||
|
|||||||
@ -223,7 +223,7 @@ export function HomePage2() {
|
|||||||
return () => window.removeEventListener('home-menu-toggle' as any, handler as any);
|
return () => window.removeEventListener('home-menu-toggle' as any, handler as any);
|
||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<div data-alt="home-navbar" className="fixed h-16 top-0 left-0 right-0 z-50">
|
<div data-alt="home-navbar" className="fixed h-16 top-0 left-0 right-0 z-[19]">
|
||||||
<div className="mx-auto h-full">
|
<div className="mx-auto h-full">
|
||||||
<div className="flex h-full items-center justify-center px-4 sm:px-6 py-3 bg-black/60 backdrop-blur-md border-b border-white/10">
|
<div className="flex h-full items-center justify-center px-4 sm:px-6 py-3 bg-black/60 backdrop-blur-md border-b border-white/10">
|
||||||
{/* 桌面端菜单(居中,仅三个项) */}
|
{/* 桌面端菜单(居中,仅三个项) */}
|
||||||
@ -1249,34 +1249,16 @@ function HomeModule5() {
|
|||||||
|
|
||||||
const handleSubscribe = async (planName: string) => {
|
const handleSubscribe = async (planName: string) => {
|
||||||
localStorage.setItem("callBackUrl", pathname);
|
localStorage.setItem("callBackUrl", pathname);
|
||||||
try {
|
// 改为直接携带参数打开 pay-redirect,由其内部完成创建与跳转
|
||||||
// 使用新的Checkout Session方案(更简单!)
|
const url = `/pay-redirect?type=subscription&plan=${encodeURIComponent(planName)}&billing=${encodeURIComponent(billingType)}`;
|
||||||
const { createCheckoutSession, redirectToCheckout } = await import(
|
const win = window.open(url, "_blank");
|
||||||
"@/lib/stripe"
|
// 通知当前窗口等待支付(显示loading模态框)
|
||||||
);
|
window.postMessage({
|
||||||
|
type: 'waiting-payment',
|
||||||
// 从localStorage获取当前用户信息
|
paymentType: 'subscription',
|
||||||
const User = JSON.parse(localStorage.getItem("currentUser") || "{}");
|
}, '*');
|
||||||
|
if (!win) {
|
||||||
if (!User.id) {
|
throw new Error("Unable to open redirect window, please check popup settings");
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -301,7 +301,7 @@ export function H5MediaViewer({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-alt="open-catalog-button"
|
data-alt="open-catalog-button"
|
||||||
className="fixed bottom-4 right-4 z-[60] w-12 h-12 rounded-full bg-gradient-to-br from-blue-500 to-blue-600 text-white shadow-lg flex items-center justify-center active:scale-95"
|
className="fixed bottom-[6rem] right-4 z-[60] w-12 h-12 rounded-full bg-gradient-to-br from-blue-500 to-blue-600 text-white shadow-lg flex items-center justify-center active:scale-95"
|
||||||
aria-label="open-catalog"
|
aria-label="open-catalog"
|
||||||
onClick={() => setIsCatalogOpen(true)}
|
onClick={() => setIsCatalogOpen(true)}
|
||||||
>
|
>
|
||||||
@ -371,13 +371,13 @@ export function H5MediaViewer({
|
|||||||
|
|
||||||
// 其他阶段:使用 Carousel
|
// 其他阶段:使用 Carousel
|
||||||
return (
|
return (
|
||||||
<div ref={rootRef} data-alt="h5-media-viewer" className="w-[100vw] relative px-4">
|
<div ref={rootRef} data-alt="h5-media-viewer" className={`w-[100vw] relative ${stage === 'final_video' ? '' : 'px-4'}`}>
|
||||||
{/* 左侧最终视频缩略图栏(H5) 视频暂停时展示 */}
|
{/* 左侧最终视频缩略图栏(H5) 视频暂停时展示 */}
|
||||||
{taskObject?.final?.url && !isPlaying && (
|
{taskObject?.final?.url && !isPlaying && (
|
||||||
<div className="absolute left-4 top-0 z-[60]" data-alt="final-sidebar-h5">
|
<div className={`absolute left-0 top-4 z-[10] ${stage === 'final_video' ? 'left-0' : 'left-4'}`} data-alt="final-sidebar-h5">
|
||||||
<div className="flex items-start">
|
<div className="flex items-start">
|
||||||
{isFinalBarOpen && (
|
{isFinalBarOpen && (
|
||||||
<div className="w-20 max-h-[50vh] overflow-y-auto rounded-md backdrop-blur-lg bg-black/30 border border-white/20 shadow-xl p-1 mr-2" data-alt="final-thumbnails">
|
<div className="w-[3rem] max-h-[50vh] overflow-y-auto rounded-md backdrop-blur-lg bg-black/30 border border-white/20 shadow-xl p-1" data-alt="final-thumbnails">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onSelectView && onSelectView('final')}
|
onClick={() => onSelectView && onSelectView('final')}
|
||||||
@ -491,8 +491,11 @@ export function H5MediaViewer({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<style jsx global>{`
|
<style jsx global>{`
|
||||||
[data-alt='carousel-wrapper'] .slick-slide { display: block; }
|
[data-alt='carousel-wrapper'] .slick-slide { display: flex !important;justify-content: center; }
|
||||||
|
.slick-slider { height: 100% !important;display: flex !important; }
|
||||||
|
.ant-carousel { height: 100% !important; }
|
||||||
.slick-list { width: 100%;height: 100% !important;max-height: calc(100vh - 20rem); }
|
.slick-list { width: 100%;height: 100% !important;max-height: calc(100vh - 20rem); }
|
||||||
|
.slick-track { display: flex !important; align-items: center;height: 100% !important; }
|
||||||
`}</style>
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -125,14 +125,14 @@ const H5TaskInfo: React.FC<H5TaskInfoProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-alt="h5-header"
|
data-alt="h5-header"
|
||||||
className={`absolute top-0 left-0 right-0 z-[50] pr-1 ${className || ''}`}
|
className={`${className || ''}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-alt="h5-header-bar"
|
data-alt="h5-header-bar"
|
||||||
className="flex items-start gap-3"
|
className="flex items-start gap-3"
|
||||||
>
|
>
|
||||||
{/* 左侧标题区域 */}
|
{/* 左侧标题区域 */}
|
||||||
<div data-alt="title-area" className="flex-1 min-w-0 bg-gradient-to-b from-slate-900/80 via-slate-900/40 to-transparent backdrop-blur-sm rounded-lg py-4 px-4">
|
<div data-alt="title-area" className="flex-1 min-w-0">
|
||||||
<h1
|
<h1
|
||||||
data-alt="title"
|
data-alt="title"
|
||||||
className="text-white text-lg font-bold"
|
className="text-white text-lg font-bold"
|
||||||
|
|||||||
@ -356,7 +356,7 @@ export function ThumbnailGrid({
|
|||||||
<div
|
<div
|
||||||
ref={thumbnailsRef}
|
ref={thumbnailsRef}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className={`w-full h-full grid grid-flow-col gap-2 overflow-x-auto hide-scrollbar px-1 py-1 cursor-grab active:cursor-grabbing focus:outline-none select-none auto-cols-[${cols}] ${aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? '' : '!auto-cols-max'}`}
|
className={`w-full h-full grid grid-flow-col gap-2 overflow-x-auto hide-scrollbar px-1 py-1 cursor-grab active:cursor-grabbing focus:outline-none select-none auto-cols-[${cols}] ${aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? '' : '!auto-cols-min'}`}
|
||||||
autoFocus
|
autoFocus
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
|
|||||||
@ -102,7 +102,7 @@ const H5ProgressToastUI: React.FC<UIProps> = ({ open, title, progress }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-1 flex items-center justify-between text-xs text-white/70">
|
<div className="mt-1 flex items-center justify-between text-xs text-white/70">
|
||||||
<span data-alt="percent">{pct}%</span>
|
<span data-alt="percent">{pct}%</span>
|
||||||
<span data-alt="hint">{pct >= 100 ? '即将完成' : ''}</span>
|
<span data-alt="hint">{pct >= 100 ? 'completed' : ''}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -140,7 +140,7 @@ export function ScriptModal({ isOpen, onClose, currentStage = 0, roles, currentL
|
|||||||
|
|
||||||
{/* 弹窗内容 */}
|
{/* 弹窗内容 */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center"
|
className="fixed inset-0 z-[61] flex items-center justify-center"
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
|
|||||||
@ -2,16 +2,16 @@ import { useState, useEffect } from 'react';
|
|||||||
|
|
||||||
// 定义设备类型枚举
|
// 定义设备类型枚举
|
||||||
export enum DeviceType {
|
export enum DeviceType {
|
||||||
MOBILE = 'mobile', // 手机
|
MOBILE = 'mobile',
|
||||||
TABLET = 'tablet', // 平板
|
TABLET = 'tablet',
|
||||||
DESKTOP = 'desktop' // 桌面端
|
DESKTOP = 'desktop'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 定义屏幕断点
|
// 定义屏幕断点
|
||||||
const BREAKPOINTS = {
|
const BREAKPOINTS = {
|
||||||
MOBILE: 480, // 0-480px 为手机
|
MOBILE: 480,
|
||||||
TABLET: 1024, // 481-1024px 为平板
|
TABLET: 1024,
|
||||||
DESKTOP: 1025 // 1025px 及以上为桌面端
|
DESKTOP: 1025
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useDeviceType() {
|
export function useDeviceType() {
|
||||||
@ -22,35 +22,39 @@ export function useDeviceType() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
/**
|
|
||||||
* 根据窗口宽度判断设备类型
|
|
||||||
*/
|
|
||||||
const getDeviceType = (width: number): DeviceType => {
|
const getDeviceType = (width: number): DeviceType => {
|
||||||
if (width <= BREAKPOINTS.MOBILE) return DeviceType.MOBILE;
|
if (width <= BREAKPOINTS.MOBILE) return DeviceType.MOBILE;
|
||||||
if (width <= BREAKPOINTS.TABLET) return DeviceType.TABLET;
|
if (width <= BREAKPOINTS.TABLET) return DeviceType.TABLET;
|
||||||
return DeviceType.DESKTOP;
|
return DeviceType.DESKTOP;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理窗口大小变化
|
|
||||||
*/
|
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
const width = window.innerWidth;
|
const width = window.innerWidth;
|
||||||
const height = window.innerHeight;
|
const height = window.innerHeight;
|
||||||
|
|
||||||
setWindowSize({ width, height });
|
setWindowSize({ width, height });
|
||||||
setDeviceType(getDeviceType(width));
|
setDeviceType(getDeviceType(width));
|
||||||
|
|
||||||
|
// 移动端动态视口高度处理
|
||||||
|
if (width <= BREAKPOINTS.TABLET) {
|
||||||
|
const vh = height * 0.01;
|
||||||
|
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 初始化设备类型
|
// 初始化
|
||||||
handleResize();
|
handleResize();
|
||||||
|
|
||||||
// 添加窗口大小变化监听
|
// 添加事件监听
|
||||||
window.addEventListener('resize', handleResize);
|
window.addEventListener('resize', handleResize);
|
||||||
|
window.addEventListener('orientationchange', () => {
|
||||||
|
// 延迟处理以确保获取正确的视口尺寸
|
||||||
|
setTimeout(handleResize, 100);
|
||||||
|
});
|
||||||
|
|
||||||
// 清理监听器
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', handleResize);
|
window.removeEventListener('resize', handleResize);
|
||||||
|
window.removeEventListener('orientationchange', handleResize);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -59,6 +63,8 @@ export function useDeviceType() {
|
|||||||
windowSize,
|
windowSize,
|
||||||
isMobile: deviceType === DeviceType.MOBILE,
|
isMobile: deviceType === DeviceType.MOBILE,
|
||||||
isTablet: deviceType === DeviceType.TABLET,
|
isTablet: deviceType === DeviceType.TABLET,
|
||||||
isDesktop: deviceType === DeviceType.DESKTOP
|
isDesktop: deviceType === DeviceType.DESKTOP,
|
||||||
|
/** 是否为移动端设备(包括平板) */
|
||||||
|
isMobileDevice: deviceType === DeviceType.MOBILE || deviceType === DeviceType.TABLET
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
64
hooks/useSafeArea.ts
Normal file
64
hooks/useSafeArea.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
interface SafeAreaInsets {
|
||||||
|
top: number;
|
||||||
|
right: number;
|
||||||
|
bottom: number;
|
||||||
|
left: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSafeArea() {
|
||||||
|
const [safeAreaInsets, setSafeAreaInsets] = useState<SafeAreaInsets>({
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const [viewportHeight, setViewportHeight] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateSafeArea = () => {
|
||||||
|
// 获取 CSS 环境变量
|
||||||
|
const computedStyle = getComputedStyle(document.documentElement);
|
||||||
|
|
||||||
|
const top = parseInt(computedStyle.getPropertyValue('--sat').replace('px', '')) || 0;
|
||||||
|
const right = parseInt(computedStyle.getPropertyValue('--sar').replace('px', '')) || 0;
|
||||||
|
const bottom = parseInt(computedStyle.getPropertyValue('--sab').replace('px', '')) || 0;
|
||||||
|
const left = parseInt(computedStyle.getPropertyValue('--sal').replace('px', '')) || 0;
|
||||||
|
|
||||||
|
setSafeAreaInsets({ top, right, bottom, left });
|
||||||
|
|
||||||
|
// 设置动态视口高度
|
||||||
|
const vh = window.innerHeight;
|
||||||
|
setViewportHeight(vh);
|
||||||
|
document.documentElement.style.setProperty('--vh', `${vh * 0.01}px`);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateSafeArea();
|
||||||
|
|
||||||
|
window.addEventListener('resize', updateSafeArea);
|
||||||
|
window.addEventListener('orientationchange', updateSafeArea);
|
||||||
|
|
||||||
|
// 延迟更新以处理移动端浏览器地址栏变化
|
||||||
|
const timeoutId = setTimeout(updateSafeArea, 500);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', updateSafeArea);
|
||||||
|
window.removeEventListener('orientationchange', updateSafeArea);
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
safeAreaInsets,
|
||||||
|
viewportHeight,
|
||||||
|
/** 获取考虑安全区域的样式 */
|
||||||
|
getSafeAreaStyle: (includeBottom = true) => ({
|
||||||
|
paddingTop: `max(1rem, ${safeAreaInsets.top}px)`,
|
||||||
|
paddingRight: `max(1rem, ${safeAreaInsets.right}px)`,
|
||||||
|
paddingBottom: includeBottom ? `max(1rem, ${safeAreaInsets.bottom}px)` : undefined,
|
||||||
|
paddingLeft: `max(1rem, ${safeAreaInsets.left}px)`,
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -90,6 +90,20 @@ module.exports = {
|
|||||||
'100': '100ms',
|
'100': '100ms',
|
||||||
'200': '200ms',
|
'200': '200ms',
|
||||||
'300': '300ms',
|
'300': '300ms',
|
||||||
|
},
|
||||||
|
spacing: {
|
||||||
|
'safe-top': 'env(safe-area-inset-top)',
|
||||||
|
'safe-right': 'env(safe-area-inset-right)',
|
||||||
|
'safe-bottom': 'env(safe-area-inset-bottom)',
|
||||||
|
'safe-left': 'env(safe-area-inset-left)',
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
'dvh': '100dvh',
|
||||||
|
'safe-screen': 'calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom))',
|
||||||
|
},
|
||||||
|
minHeight: {
|
||||||
|
'dvh': '100dvh',
|
||||||
|
'safe-screen': 'calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom))',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user