From bc0269983d1c6317b060375b1a9bf33b00954072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=97=E6=9E=B3?= <7854742+wang_rumeng@user.noreply.gitee.com> Date: Sun, 28 Sep 2025 18:07:53 +0800 Subject: [PATCH] =?UTF-8?q?H5=E9=A1=B6=E9=83=A8=E5=AF=BC=E8=88=AA=E6=A0=8F?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E4=B8=8E=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/page.tsx | 21 +- app/share/page.tsx | 31 +- components/layout/H5TopBar.css | 42 +++ components/layout/H5TopBar.tsx | 495 +++++++++++++++++++++++++ components/layout/dashboard-layout.tsx | 12 +- components/layout/sidebar.tsx | 29 +- components/layout/signin-box.tsx | 4 +- components/layout/type.ts | 22 ++ components/pages/home-page2.tsx | 24 +- components/pages/usage-view.tsx | 58 +-- 10 files changed, 639 insertions(+), 99 deletions(-) create mode 100644 components/layout/H5TopBar.css create mode 100644 components/layout/H5TopBar.tsx create mode 100644 components/layout/type.ts diff --git a/app/page.tsx b/app/page.tsx index 782e582..05e71c3 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,11 +1,30 @@ +"use client"; + +import { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; import { TopBar } from "@/components/layout/top-bar"; import { HomePage2 } from "@/components/pages/home-page2"; +import { isAuthenticated } from '@/lib/auth'; +import { useDeviceType } from '@/hooks/useDeviceType'; +import H5TopBar from '@/components/layout/H5TopBar'; export default function Home() { + const router = useRouter(); + const { deviceType, isMobile, isTablet, isDesktop } = useDeviceType(); + + useEffect(() => { + if (isAuthenticated()) { + router.replace('/movies'); + } + }, [router]); return ( <> - + {isMobile || isTablet ? ( + + ) : ( + + )} ); diff --git a/app/share/page.tsx b/app/share/page.tsx index ae5ad57..f81c356 100644 --- a/app/share/page.tsx +++ b/app/share/page.tsx @@ -186,10 +186,10 @@ export default function SharePage(): JSX.Element { return ( -
+
@@ -206,14 +206,7 @@ export default function SharePage(): JSX.Element {
{inviteCode ? `${'https://www.movieflow.ai'}/signup?inviteCode=${inviteCode}` : '-'}
@@ -258,21 +251,21 @@ export default function SharePage(): JSX.Element {
  • Step 1 - Share + Share

    Copy your invitation link and share it with friends.

  • Step 2 - Register + Register

    Friends click the link and register directly.

  • Step 3 - Reward + Reward

    You both receive rewards after your friend activates their account.

  • @@ -351,8 +344,8 @@ export default function SharePage(): JSX.Element { - - + + @@ -365,8 +358,8 @@ export default function SharePage(): JSX.Element { return ( - - + + {isExpanded && ( -
    Invited UsernameRegistered AtInvited UsernameRegistered At Reward
    {r.user_name}{formatLocalTime(r.created_at * 1000)}{r.user_name}{formatLocalTime(r.created_at * 1000)}
    @@ -387,8 +380,8 @@ export default function SharePage(): JSX.Element {
    - + +
    diff --git a/components/layout/H5TopBar.css b/components/layout/H5TopBar.css new file mode 100644 index 0000000..5a67a50 --- /dev/null +++ b/components/layout/H5TopBar.css @@ -0,0 +1,42 @@ +.backdrop_backdrop { + position: fixed; + top: 0; + right: 0; + left: 0; + z-index: 58; + display: grid; + height: 4rem; + pointer-events: none; +} +.backdrop_backdrop:after { + content: ""; + background: linear-gradient(180deg, rgba(0, 0, 0, .2) 60%, transparent); +} +.backdrop_backdrop:after, .backdrop_backdrop .backdrop_blur { + grid-area: 1 / 1; + transition: opacity .2s linear; +} +.backdrop_backdrop>.backdrop_blur:first-child { + backdrop-filter: blur(1px); + mask: linear-gradient(0deg, transparent, #000 8%); +} +.backdrop_backdrop>.backdrop_blur:nth-child(2) { + backdrop-filter: blur(4px); + mask: linear-gradient(0deg, transparent 8%, #000 16%); +} +.backdrop_backdrop>.backdrop_blur:nth-child(3) { + backdrop-filter: blur(8px); + mask: linear-gradient(0deg, transparent 16%, #000 24%); +} +.backdrop_backdrop>.backdrop_blur:nth-child(4) { + backdrop-filter: blur(16px); + mask: linear-gradient(0deg, transparent 24%, #000 36%); +} +.backdrop_backdrop>.backdrop_blur:nth-child(5) { + backdrop-filter: blur(24px); + mask: linear-gradient(0deg, transparent 36%, #000 48%); +} +.backdrop_backdrop>.backdrop_blur:nth-child(6) { + backdrop-filter: blur(32px); + mask: linear-gradient(0deg, transparent 48%, #000 60%); +} \ No newline at end of file diff --git a/components/layout/H5TopBar.tsx b/components/layout/H5TopBar.tsx new file mode 100644 index 0000000..8bb3e93 --- /dev/null +++ b/components/layout/H5TopBar.tsx @@ -0,0 +1,495 @@ +"use client"; + +import React, { useEffect, useMemo, useState } from 'react'; +import { usePathname, useRouter } from 'next/navigation'; +import { Menu, Rocket, LogOut, User as UserIcon, X, Info, CalendarDays } from 'lucide-react'; +import { Drawer } from 'antd'; +import { fetchTabsByCode, HomeTabItem } from '@/api/serversetting'; +import { getSigninStatus } from '@/api/signin'; +import { getCurrentUser, isAuthenticated, logoutUser } from '@/lib/auth'; +import { getUserSubscriptionInfo, createPortalSession, redirectToPortal } from '@/lib/stripe'; +import { GradientText } from '@/components/ui/gradient-text'; +import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'; +import SigninBox from './signin-box'; +import { navigationItems } from './type'; +import './H5TopBar.css'; + +interface H5TopBarProps { + /** 点击首页 tab 时的回调,用于页面内滚动 */ + onSelectHomeTab?: (key: string) => void; +} + +interface CurrentUserMinimal { + id?: string; + name?: string; + email?: string; + username?: string; +} + +/** + * 移动端顶栏(抽屉式菜单) + * - 未登录:左 LOGO → '/',右侧 Signup + 菜单;抽屉显示 homeTabs 与 登录/注册 + * - 已登录:左 LOGO → '/movies',右侧 升级图标 + 菜单;抽屉显示 用户卡片、快捷充值、入口与登出 + */ +export default function H5TopBar({ onSelectHomeTab }: H5TopBarProps) { + const router = useRouter(); + const pathname = usePathname(); + + const [isLogin, setIsLogin] = useState(false); + const [user, setUser] = useState(null); + const [credits, setCredits] = useState(0); + const [isBuyingTokens, setIsBuyingTokens] = useState(false); + const [homeTabs, setHomeTabs] = useState([]); + const [drawerOpen, setDrawerOpen] = useState(false); + const [customAmount, setCustomAmount] = useState(""); + const [isSigninModalOpen, setIsSigninModalOpen] = useState(false); + const [isManagingSubscription, setIsManagingSubscription] = useState(false); + const [subscriptionStatus, setSubscriptionStatus] = useState(""); + const [planName, setPlanName] = useState(""); + const [needsSigninBadge, setNeedsSigninBadge] = useState(false); + const [isGlassActive, setIsGlassActive] = useState(false); + + const isHome = useMemo(() => pathname === '/', [pathname]); + + useEffect(() => { + setIsLogin(isAuthenticated()); + const u = getCurrentUser(); + setUser(u || null); + }, []); + + useEffect(() => { + if (!user?.id) return; + let ignore = false; + const load = async () => { + try { + const res = await getUserSubscriptionInfo(String(user.id)); + if (!ignore && res?.data?.credits !== undefined) { + setCredits(res.data.credits); + if (typeof res.data.subscription_status === 'string') { + setSubscriptionStatus(res.data.subscription_status); + } + if (typeof res.data.plan_name === 'string') { + setPlanName(res.data.plan_name); + } + } + } catch {} + }; + load(); + return () => { + ignore = true; + }; + }, [user?.id]); + + // 仅首页时加载 homeTabs,用于未登录抽屉导航 + useEffect(() => { + if (!isHome) return; + let mounted = true; + const loadTabs = async () => { + try { + const tabs = await fetchTabsByCode('homeTab'); + if (mounted && Array.isArray(tabs)) setHomeTabs(tabs); + } catch {} + }; + loadTabs(); + return () => { + mounted = false; + }; + }, [isHome]); + + // 获取今日签到状态,未签到则显示红点 + useEffect(() => { + const loadSignin = async () => { + if (!isLogin) return; + try { + const data: any = await getSigninStatus(); + const hasSignin = !!data?.data?.has_signin; + setNeedsSigninBadge(!hasSignin); + } catch {} + }; + loadSignin(); + }, [isLogin]); + + // 首页滚动 30vh 后开启玻璃质感背景 + useEffect(() => { + if (!isHome) return; + const computeAndSet = (evt?: Event) => { + const threshold = Math.max(0, window.innerHeight * 0.3); + const winY = window.scrollY || window.pageYOffset || 0; + const docElY = (document.documentElement && document.documentElement.scrollTop) || 0; + const scrollingElY = (document.scrollingElement as any)?.scrollTop || 0; + const bodyY = (document.body && (document.body as any).scrollTop) || 0; + let currentY = Math.max(winY, docElY, scrollingElY, bodyY); + const target = evt?.target as any; + if (target && typeof target.scrollTop === 'number') { + currentY = Math.max(currentY, target.scrollTop); + } + const nextActive = currentY >= threshold; + setIsGlassActive(nextActive); + }; + // 初始计算一次 + computeAndSet(); + // 监听 window 与 document(捕获阶段,捕获内部滚动容器事件) + window.addEventListener('scroll', computeAndSet, { passive: true }); + document.addEventListener('scroll', computeAndSet, { passive: true, capture: true }); + return () => { + window.removeEventListener('scroll', computeAndSet); + document.removeEventListener('scroll', computeAndSet, { capture: true } as any); + }; + }, [isHome]); + + // 离开首页时,移除玻璃背景 + useEffect(() => { + if (!isHome) { + setIsGlassActive(false); + } else { + } + }, [isHome]); + + const handleLogoClick = () => { + if (isLogin) { + router.push('/movies'); + } else { + router.push('/'); + } + }; + + const handleUpgrade = () => { + router.push('/pricing'); + }; + + const handleBuyTokens = async (amount: number) => { + if (!user?.id) return; + setIsBuyingTokens(true); + try { + const url = `/pay-redirect?type=token&amount=${encodeURIComponent(amount)}&pkg=basic`; + window.open(url, '_blank'); + window.postMessage({ type: 'waiting-payment', paymentType: 'subscription' }, '*'); + } finally { + setIsBuyingTokens(false); + } + }; + + const handleCustomAmountBuy = async () => { + const amount = parseInt(customAmount); + if (isNaN(amount) || amount <= 0) return; + await handleBuyTokens(amount); + setCustomAmount(""); + }; + + const handleSignin = () => { + setIsSigninModalOpen(true); + }; + + const handleManageSubscription = async () => { + if (!user?.id) return; + setIsManagingSubscription(true); + try { + const response = await createPortalSession({ + user_id: String(user.id), + return_url: window.location.origin + '/dashboard', + }); + if (response.successful && response.data?.portal_url) { + redirectToPortal(response.data.portal_url); + } + } finally { + setIsManagingSubscription(false); + } + }; + + return ( + <> + + + + + + + + +
    +
    + {/* 左侧 LOGO */} +
    +

    + +

    + {/* beta标签 */} +
    + + Beta + +
    +
    + + {/* 右侧操作区 */} +
    + { + !drawerOpen && ( + <> + {isLogin ? ( + + ) : ( + + )} + + ) + } + + + {/* 菜单抽屉(antd Drawer) */} + + + setDrawerOpen(false)} + title={null} + closable + height={undefined} + bodyStyle={{ padding: 0 }} + maskClosable + zIndex={59} + // 64px 顶栏高度 + 8px 安全间距 + maskStyle={{ position: 'absolute', top: '4rem', height: 'calc(100dvh - 4rem)', backgroundColor: 'transparent' }} + styles={{ + content: { position: 'absolute', top: '4rem', height: isHome ? 'auto' : 'calc(100dvh - 4rem)' }, + body: { padding: 0 }, + header: { display: 'none' }, + mask: { position: 'absolute', top: '4rem', height: 'calc(100dvh - 4rem)', backgroundColor: 'transparent' }, + }} + rootStyle={{ zIndex: 59 }} + className="[&_.ant-drawer-content]:bg-white [&_.ant-drawer-content]:text-black dark:[&_.ant-drawer-content]:bg-[#0b0b0b] dark:[&_.ant-drawer-content]:text-white dark:[&_.ant-drawer-body]:bg-[#0b0b0b] dark:[&_.ant-drawer-body]:text-white" + > +
    + {/* 用户信息/未登录头部 */} + {isLogin ? ( +
    +
    + {(user?.username || user?.name || 'MF').slice(0, 1)} +
    +
    +
    {user?.name || user?.username || 'User'}
    +
    {user?.email}
    +
    +
    + ) : ( + <> + )} + + {/* 内容区 */} +
    + {isLogin ? ( +
    + {/* 积分中心 */} +
    +
    +
    +
    + {/* 签到 */} + + {/* 积分 */} +
    {credits} credits
    +
    +
    + +
    + + {/* 快捷充值 */} +
    + + + +
    + + {/* 自定义充值 */} +
    + setCustomAmount(e.target.value)} + placeholder="Custom amount" + className="w-[120px] h-9 px-2 text-sm bg-white/10 text-black dark:text-white placeholder-black/40 dark:placeholder-white/60 border border-black/20 dark:border-white/20 rounded focus:outline-none" + min={1} + /> + +
    +
    + + + {/* 菜单目录 */} +
    + {navigationItems.map((group) => ( + group.items.map((nav) => { + const isActive = pathname === nav.href || pathname.startsWith(nav.href + '/'); + return ( + + ); + }) + ))} +
    + + {/* 其他功能 */} +
    + + {planName !== 'none' && subscriptionStatus !== 'INACTIVE' && ( + + )} +
    +
    + ) : ( +
    + {isHome && homeTabs.length > 0 && ( +
    + {homeTabs.map((tab) => ( + + ))} +
    + )} +
    + + +
    +
    + )} +
    + + {/* 底部操作 */} + {isLogin && ( +
    + +
    + )} +
    +
    + {/* Sign-in Modal */} + { + setIsSigninModalOpen(open); + if (!open) { + // 刷新积分 + if (user?.id) { getUserSubscriptionInfo(String(user.id)).then((res:any)=>{ if(res?.data?.credits!==undefined){ setCredits(res.data.credits) } }).catch(()=>{}); } + // 关闭签到弹窗后,重新检查红点 + if (isLogin) { getSigninStatus().then((d:any)=> setNeedsSigninBadge(!d?.data?.has_signin)).catch(()=>{}); } + } + }}> + + + { + try { + if (user?.id) { + const res = await getUserSubscriptionInfo(String(user.id)); + if (res?.data?.credits !== undefined) { + setCredits(res.data.credits); + } + } + } catch {} + // 成功签到后去除红点 + setNeedsSigninBadge(false); + }} /> + + +
    +
    +
    + + ); +} + + diff --git a/components/layout/dashboard-layout.tsx b/components/layout/dashboard-layout.tsx index 0d77dd4..7d2b98e 100644 --- a/components/layout/dashboard-layout.tsx +++ b/components/layout/dashboard-layout.tsx @@ -4,6 +4,7 @@ import { useState, useEffect } from 'react'; import { Sidebar } from './sidebar'; import { TopBar } from './top-bar'; import { useDeviceType } from '@/hooks/useDeviceType'; +import H5TopBar from '@/components/layout/H5TopBar'; interface DashboardLayoutProps { children: React.ReactNode; @@ -58,15 +59,20 @@ export function DashboardLayout({ children }: DashboardLayoutProps) { return (
    - + {isMobile || isTablet ? ( + + ) : ( + + )} {isDesktop && }
    diff --git a/components/layout/sidebar.tsx b/components/layout/sidebar.tsx index f915590..293c687 100644 --- a/components/layout/sidebar.tsx +++ b/components/layout/sidebar.tsx @@ -1,44 +1,19 @@ "use client"; -import { useState } from 'react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; import { cn } from '@/public/lib/utils'; import { Button } from '@/components/ui/button'; -import { Separator } from '@/components/ui/separator'; -import { GradientText } from '@/components/ui/gradient-text'; import { - Home, - FolderOpen, - Users, - Type, - Image, - History, - ChevronLeft, - ChevronRight, - Video, - PanelsLeftBottom, - ArrowLeftToLine, - BookHeart, - PanelRightClose, - Gift + PanelRightClose } from 'lucide-react'; +import { navigationItems } from './type'; interface SidebarProps { collapsed: boolean; onToggle: (collapsed: boolean) => void; } -const navigationItems = [ - { - title: 'Main', - items: [ - { name: 'My Portfolio', href: '/movies', icon: BookHeart }, - { name: 'Share', href: '/share', icon: Gift }, - ], - } -]; - export function Sidebar({ collapsed, onToggle }: SidebarProps) { const pathname = usePathname(); diff --git a/components/layout/signin-box.tsx b/components/layout/signin-box.tsx index 004f8b7..d616a93 100644 --- a/components/layout/signin-box.tsx +++ b/components/layout/signin-box.tsx @@ -7,7 +7,7 @@ import { Coins, Trophy, HelpCircle } from "lucide-react" import { getSigninStatus, performSignin, SigninData } from "@/api/signin" -export default function SigninPage() { +export default function SigninPage({ onSuccess }: { onSuccess?: () => void } = {}) { const [signinData, setSigninData] = useState({ has_signin: false, credits: 0 @@ -50,6 +50,8 @@ export default function SigninPage() { if (response.successful) { // Refresh status after successful signin await fetchSigninStatus() + // Notify parent to refresh credits + try { onSuccess && onSuccess() } catch {} } } catch (error) { console.error('Signin failed:', error) diff --git a/components/layout/type.ts b/components/layout/type.ts new file mode 100644 index 0000000..9660c8b --- /dev/null +++ b/components/layout/type.ts @@ -0,0 +1,22 @@ +import { BookHeart, Gift } from "lucide-react"; + +interface NavigationItem { + name: string; + href: string; + icon: any; +} + +interface Navigations { + title: string; + items: NavigationItem[]; +} + +export const navigationItems: Navigations[] = [ + { + title: 'Main', + items: [ + { name: 'My Portfolio', href: '/movies', icon: BookHeart }, + { name: 'Share', href: '/share', icon: Gift }, + ], + } +]; \ No newline at end of file diff --git a/components/pages/home-page2.tsx b/components/pages/home-page2.tsx index a8e9664..a61440c 100644 --- a/components/pages/home-page2.tsx +++ b/components/pages/home-page2.tsx @@ -20,6 +20,7 @@ import LazyLoad from "react-lazyload"; import { fetchSubscriptionPlans, SubscriptionPlan } from "@/lib/stripe"; import VideoCoverflow from "@/components/ui/VideoCoverflow"; import { fetchTabsByCode, HomeTabItem } from "@/api/serversetting"; +import H5TopBar from "@/components/layout/H5TopBar"; import { useCallbackModal } from "@/app/layout"; import { useDeviceType } from "@/hooks/useDeviceType"; @@ -241,26 +242,9 @@ export function HomePage2() { ))}
    - {/* 移动端开关移至 TopBar,保留占位对齐 */} + {/* 移动端交由 H5TopBar 控制 */}
    - {/* 移动端下拉(仅三个项) */} - {menuOpen && ( -
    -
    - {tabsToRender.map((tab) => ( - - ))} -
    -
    - )} ); @@ -268,7 +252,9 @@ export function HomePage2() { return (
    - + {/* 移动端顶部导航(抽屉式) */} + scrollToSection(key as any)} /> + {/* */} diff --git a/components/pages/usage-view.tsx b/components/pages/usage-view.tsx index 9865023..6f3a2bd 100644 --- a/components/pages/usage-view.tsx +++ b/components/pages/usage-view.tsx @@ -191,9 +191,9 @@ const UsageView: React.FC = () => { }, []); return ( -
    +
    -

    Credit Usage Details

    +

    Credit Usage Details

    {([7, 30, 90] as PeriodDays[]).map((d) => (
    -
    +
    Period: {periodLabel || "-"} days
    -
    -
    +
    +
    - - - - + + + + {loading ? ( - + ) : error ? ( - + ) : items.length === 0 ? ( - + ) : ( items.map((it, idx) => { @@ -248,7 +248,7 @@ const UsageView: React.FC = () => { return ( - - - - {isExpanded && it.project_info && ( -
    KindCreditsFromDateKindCreditsFromDate
    Loading...Loading...
    --
    --
    +
    {it?.transaction_type || '-'} {it?.project_info ? ( @@ -272,47 +272,47 @@ const UsageView: React.FC = () => { ) : null}
    + {Number.isFinite(it?.amount as number) ? `${it.change_type === 'INCREASE' ? '+' : it.change_type === 'DECREASE' ? '-' : ''}${it.amount}` : '-'} + {formatSource(it?.source_type)} + {it?.created_at ? dateFormatter.format(new Date(it.created_at)) : '-'}
    -
    +
    +
    Project: {it.project_info.project_name || '-'}
    ID: {it.project_info.project_id || '-'}
    - +
    {(it.project_info.videos || []).length === 0 ? ( - + ) : ( it.project_info.videos.map((v, vIdx) => ( - - - - @@ -332,7 +332,7 @@ const UsageView: React.FC = () => {
    --
    + {v.video_name && v.video_name.trim() ? v.video_name : `video${vIdx + 1}`} + {Number.isFinite(v.amount as number) ? `${v.change_type === 'INCREASE' ? '+' : v.change_type === 'DECREASE' ? '-' : ''}${v.amount}` : '-'} + {formatSource(v.source_type)} + {v.created_at ? dateFormatter.format(new Date(v.created_at)) : '-'}
    -
    +
    Total {Number.isFinite(total) ? total : 0}
    @@ -342,20 +342,20 @@ const UsageView: React.FC = () => { onClick={handlePrev} disabled={!canPrev} className={ - "rounded-md px-3 py-1.5 transition-colors " + + "rounded-md px-2 py-1 sm:px-3 sm:py-1.5 transition-colors " + (canPrev ? "bg-white/10 hover:bg-white/20" : "bg-white/5 text-white/40 cursor-not-allowed") } data-alt="prev-page" > Previous - Page {page} + Page {page}