H5顶部导航栏设计与实现

This commit is contained in:
北枳 2025-09-28 18:07:53 +08:00
parent 967fe95ccc
commit bc0269983d
10 changed files with 639 additions and 99 deletions

View File

@ -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 ? (
<H5TopBar />
) : (
<TopBar collapsed={true} />
)}
<HomePage2 />
</>
);

View File

@ -186,10 +186,10 @@ export default function SharePage(): JSX.Element {
return (
<DashboardLayout>
<div data-alt="share-page" className="w-full h-full overflow-y-auto bg-black text-white">
<div data-alt="share-page" className="w-full h-full overflow-y-auto overflow-x-hidden bg-black text-white">
<div
data-alt="container"
className="w-full max-w-[95%] mx-auto px-4 py-10 sm:px-6 md:px-8 lg:px-12 xl:px-16 2xl:px-20"
className="w-full max-w-[100%] mx-auto px-4 py-2 sm:px-6 md:px-8 lg:px-12 xl:px-16 2xl:px-20"
>
<header data-alt="page-header" className="mb-8 flex items-end justify-between">
<div data-alt="title-box">
@ -206,14 +206,7 @@ export default function SharePage(): JSX.Element {
<div data-alt="link-container" className="relative w-full max-w-2xl overflow-hidden rounded-md border border-white/20 bg-white/10">
<div
data-alt="link-content"
className="relative px-4 py-2 text-sm font-mono text-white/90 whitespace-nowrap overflow-hidden"
style={{
background: 'linear-gradient(90deg, rgba(255,255,255,0.9) 0%, rgba(255,255,255,0.7) 50%, rgba(255,255,255,0.3) 70%, rgba(255,255,255,0.1) 90%, rgba(255,255,255,0) 100%)',
WebkitBackgroundClip: 'text',
backgroundClip: 'text',
maskImage: 'linear-gradient(90deg, rgba(0,0,0,1) 0%, rgba(0,0,0,1) 70%, rgba(0,0,0,0.5) 85%, rgba(0,0,0,0) 100%)',
WebkitMaskImage: 'linear-gradient(90deg, rgba(0,0,0,1) 0%, rgba(0,0,0,1) 70%, rgba(0,0,0,0.5) 85%, rgba(0,0,0,0) 100%)'
}}
className="relative px-4 py-2 text-xs sm:text-sm font-mono text-white/90 break-all sm:truncate"
>
{inviteCode ? `${'https://www.movieflow.ai'}/signup?inviteCode=${inviteCode}` : '-'}
</div>
@ -258,21 +251,21 @@ export default function SharePage(): JSX.Element {
<li data-alt="step" className="rounded-md p-4">
<div data-alt="step-header" className="flex items-center justify-between">
<span className="text-sm font-medium text-custom-blue/50">Step 1</span>
<span className="rounded bg-white/10 px-2 py-0.5 text-xs text-white bg-custom-purple/50">Share</span>
<span className="rounded px-2 py-0.5 text-xs text-white bg-custom-purple/50">Share</span>
</div>
<p data-alt="step-desc" className="mt-2 text-sm text-white/70">Copy your invitation link and share it with friends.</p>
</li>
<li data-alt="step" className="rounded-md p-4">
<div data-alt="step-header" className="flex items-center justify-between">
<span className="text-sm font-medium text-custom-blue/50">Step 2</span>
<span className="rounded bg-white/10 px-2 py-0.5 text-xs text-white bg-custom-purple/50">Register</span>
<span className="rounded px-2 py-0.5 text-xs text-white bg-custom-purple/50">Register</span>
</div>
<p data-alt="step-desc" className="mt-2 text-sm text-white/70">Friends click the link and register directly.</p>
</li>
<li data-alt="step" className="rounded-md p-4">
<div data-alt="step-header" className="flex items-center justify-between">
<span className="text-sm font-medium text-custom-blue/50">Step 3</span>
<span className="rounded bg-white/10 px-2 py-0.5 text-xs text-white bg-custom-purple/50">Reward</span>
<span className="rounded px-2 py-0.5 text-xs text-white bg-custom-purple/50">Reward</span>
</div>
<p data-alt="step-desc" className="mt-2 text-sm text-white/70">You both receive rewards after your friend activates their account.</p>
</li>
@ -351,8 +344,8 @@ export default function SharePage(): JSX.Element {
<table data-alt="records-table" className="min-w-full divide-y divide-white/10 table-fixed">
<thead data-alt="table-head" className="bg-black">
<tr data-alt="table-head-row">
<th className="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-white/60 w-48">Invited Username</th>
<th className="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-white/60 w-56">Registered At</th>
<th className="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-white/60 sm:w-48">Invited Username</th>
<th className="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-white/60 sm:w-56">Registered At</th>
<th className="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-white/60">Reward</th>
</tr>
</thead>
@ -365,8 +358,8 @@ export default function SharePage(): JSX.Element {
return (
<React.Fragment key={r.user_id}>
<tr data-alt="table-row" className="hover:bg-white/5">
<td className="px-4 py-3 text-sm text-white w-48">{r.user_name}</td>
<td className="px-4 py-3 text-sm text-white/80 w-56 whitespace-nowrap">{formatLocalTime(r.created_at * 1000)}</td>
<td className="px-4 py-3 text-sm text-white sm:w-48">{r.user_name}</td>
<td className="px-4 py-3 text-sm text-white/80 whitespace-nowrap sm:w-56">{formatLocalTime(r.created_at * 1000)}</td>
<td className="px-4 py-3 text-sm text-white/90">
<div data-alt="reward-cell" className="flex items-center justify-between gap-3">
<div data-alt="reward-summary" className="flex-1 truncate text-[#FFCC6D]">
@ -387,8 +380,8 @@ export default function SharePage(): JSX.Element {
</tr>
{isExpanded && (
<tr data-alt="row-details">
<td className="px-4 py-0 w-48" />
<td className="px-4 py-0 w-56" />
<td className="px-4 py-0 sm:w-48" />
<td className="px-4 py-0 sm:w-56" />
<td className="px-4 py-3 bg-white/5">
<div data-alt="details-wrapper" className="overflow-x-auto">
<table data-alt="reward-subtable" className="min-w-[320px] text-sm">

View File

@ -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%);
}

View File

@ -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<boolean>(false);
const [user, setUser] = useState<CurrentUserMinimal | null>(null);
const [credits, setCredits] = useState<number>(0);
const [isBuyingTokens, setIsBuyingTokens] = useState<boolean>(false);
const [homeTabs, setHomeTabs] = useState<HomeTabItem[]>([]);
const [drawerOpen, setDrawerOpen] = useState<boolean>(false);
const [customAmount, setCustomAmount] = useState<string>("");
const [isSigninModalOpen, setIsSigninModalOpen] = useState(false);
const [isManagingSubscription, setIsManagingSubscription] = useState(false);
const [subscriptionStatus, setSubscriptionStatus] = useState<string>("");
const [planName, setPlanName] = useState<string>("");
const [needsSigninBadge, setNeedsSigninBadge] = useState<boolean>(false);
const [isGlassActive, setIsGlassActive] = useState<boolean>(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 (
<>
<span className={`backdrop_backdrop ${isHome && isGlassActive ? 'grid' : 'hidden'}`}>
<span className='backdrop_blur'></span>
<span className='backdrop_blur'></span>
<span className='backdrop_blur'></span>
<span className='backdrop_blur'></span>
<span className='backdrop_blur'></span>
<span className='backdrop_blur'></span>
</span>
<div data-alt="h5-topbar" className={`fixed left-0 right-0 top-0 h-16 header z-[60] ${drawerOpen ? 'bg-[#0b0b0b] pointer-events-auto' : ''}` }>
<div data-alt="bar" className="h-14 px-3 flex items-center justify-between">
{/* 左侧 LOGO */}
<div
className={`flex items-center cursor-pointer space-x-1 link-logo roll event-on`}
onClick={handleLogoClick}
>
<h1 className="logo text-2xl font-bold">
<GradientText
text="MovieFlow"
startPercentage={30}
endPercentage={70}
/>
</h1>
{/* beta标签 */}
<div className="relative transform translate-y-[-1px]">
<span className="inline-flex items-center px-1.5 py-0.5 text-[10px] font-semibold tracking-wider text-[rgb(212 202 202)] border border-[rgba(106,244,249,0.2)] rounded-full shadow-[0_0_10px_rgba(106,244,249,0.1)]">
Beta
</span>
</div>
</div>
{/* 右侧操作区 */}
<div data-alt="actions" className="flex items-center gap-2">
{
!drawerOpen && (
<>
{isLogin ? (
<button
data-alt="upgrade-icon-button"
className="h-9 w-9 rounded-full flex items-center justify-center text-gray-800 bg-gray-100 hover:bg-gray-200 border border-black/10 dark:text-white dark:bg-white/10 dark:hover:bg-white/20 dark:border-white/20"
onClick={handleUpgrade}
aria-label="Upgrade"
>
<Rocket className="h-4 w-4" />
</button>
) : (
<button
data-alt="signup-button"
className="px-3 h-9 rounded-full text-sm bg-white text-black hover:bg-white/90 border border-black/10 dark:bg-white/10 dark:text-white dark:hover:bg-white/20 dark:border-white/20"
onClick={() => router.push('/signup')}
>
Sign up
</button>
)}
</>
)
}
{/* 菜单抽屉antd Drawer */}
<button
data-alt="menu-trigger"
className="relative h-9 w-9 rounded-full flex items-center justify-center bg-gray-100 hover:bg-gray-200 text-gray-800 border border-black/10 dark:bg-white/10 dark:hover:bg-white/20 dark:text-white dark:border-white/20"
aria-expanded={drawerOpen}
onClick={() => setDrawerOpen((v) => !v)}
>
{
drawerOpen ? <X className="h-4 w-4" /> : <Menu className="h-4 w-4" />
}
{needsSigninBadge && !drawerOpen && (
<span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-red-500 border border-white dark:border-[#0b0b0b]"></span>
)}
</button>
<Drawer
placement="top"
open={drawerOpen}
onClose={() => 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"
>
<div data-alt="drawer-container" className="h-full flex flex-col">
{/* 用户信息/未登录头部 */}
{isLogin ? (
<div data-alt="user-header" className="p-4 border-b border-t border-black/10 dark:border-white/10 flex items-center gap-3">
<div className="h-10 w-10 rounded-full bg-[#C73BFF] text-white flex items-center justify-center">
{(user?.username || user?.name || 'MF').slice(0, 1)}
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium truncate">{user?.name || user?.username || 'User'}</div>
<div className="text-xs text-black/60 dark:text-white/60 truncate">{user?.email}</div>
</div>
</div>
) : (
<></>
)}
{/* 内容区 */}
<div data-alt="drawer-content" className="flex-1 overflow-y-auto">
{isLogin ? (
<div className="p-4 space-y-4">
{/* 积分中心 */}
<div data-alt="wallet-card" className="rounded-xl border border-black/10 dark:border-white/10 p-4 bg-white dark:bg-white/5">
<div className="flex items-center justify-between">
<div>
<div className="flex items-center gap-2">
{/* 签到 */}
<button
type="button"
onClick={handleSignin}
className="w-9 h-9 p-2 rounded-full bg-white/10 backdrop-blur-sm hover:bg-white/20 transition-colors relative"
data-alt="share-entry-button"
title="Share"
>
<span className={`inline-block ${needsSigninBadge ? 'motion-safe:animate-wiggle' : ''}`}>
<CalendarDays className="h-5 w-5 text-white" />
</span>
{needsSigninBadge && (
<span className="absolute top-0 right-0 h-2 w-2 rounded-full bg-red-500 border border-white dark:border-[#0b0b0b]"></span>
)}
</button>
{/* 积分 */}
<div className="text-2xl font-semibold mt-1">{credits} <span className="text-xs text-black/60 dark:text-white/60">credits</span></div>
</div>
</div>
<button
className="relative h-8 w-8 rounded-full bg-white/10 dark:bg-white/10 flex items-center justify-center hover:bg-white/20"
onClick={() => router.push("/usage")}
title="Usage"
>
<Info className="h-4 w-4" />
</button>
</div>
{/* 快捷充值 */}
<div className="mt-3 flex flex-wrap gap-2">
<button className="px-2 py-1 text-xs rounded bg-black text-white" disabled={isBuyingTokens} onClick={() => handleBuyTokens(100)}>+100 ($1)</button>
<button className="px-2 py-1 text-xs rounded bg-black text-white" disabled={isBuyingTokens} onClick={() => handleBuyTokens(500)}>+500 ($5)</button>
<button className="px-2 py-1 text-xs rounded bg-black text-white" disabled={isBuyingTokens} onClick={() => handleBuyTokens(1000)}>+1000 ($10)</button>
</div>
{/* 自定义充值 */}
<div className="mt-2 flex items-center gap-2">
<input
type="number"
value={customAmount}
onChange={(e) => 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}
/>
<button
className="px-3 h-9 rounded bg-black text-white text-sm disabled:opacity-50"
disabled={!customAmount || parseInt(customAmount) <= 0 || isBuyingTokens}
onClick={handleCustomAmountBuy}
>
Buy
</button>
</div>
</div>
{/* 菜单目录 */}
<div data-alt="menu-links" className="rounded-xl border border-black/10 dark:border-white/10 divide-y divide-black/10 dark:divide-white/10 overflow-hidden">
{navigationItems.map((group) => (
group.items.map((nav) => {
const isActive = pathname === nav.href || pathname.startsWith(nav.href + '/');
return (
<button
key={nav.href}
data-alt={`link-${nav.name.toLowerCase()}`}
aria-current={isActive ? 'page' : undefined}
className={`w-full flex items-center gap-3 px-4 py-3 text-left hover:bg-gray-50 dark:hover:bg-white/10 ${isActive ? 'bg-gray-100 dark:bg-white/10' : ''}`}
onClick={() => router.push(nav.href)}
>
<nav.icon className="h-4 w-4" />
<span>{nav.name}</span>
</button>
);
})
))}
</div>
{/* 其他功能 */}
<div className="space-y-2">
<button data-alt="upgrade-button" className="w-full h-10 rounded-full border border-black/20 dark:border-white/20 hover:bg-gray-50 dark:hover:bg-white/10" onClick={handleUpgrade}>Upgrade</button>
{planName !== 'none' && subscriptionStatus !== 'INACTIVE' && (
<button data-alt="manage-button" className="w-full h-10 rounded-full border border-black/20 dark:border-white/20 hover:bg-gray-50 dark:hover:bg-white/10" onClick={handleManageSubscription} disabled={isManagingSubscription}>Manage</button>
)}
</div>
</div>
) : (
<div className="p-4 space-y-4 pt-0">
{isHome && homeTabs.length > 0 && (
<div data-alt="home-tabs" className="rounded-xl border border-black/10 dark:border-white/10 overflow-hidden">
{homeTabs.map((tab) => (
<button
key={tab.title}
data-alt={`home-tab-${tab.title.toLowerCase()}`}
className="w-full text-left px-4 py-3 border-b border-black/10 dark:border-white/10 last:border-b-0 hover:bg-gray-50 dark:hover:bg-white/10"
onClick={() => {
if (onSelectHomeTab) onSelectHomeTab(tab.title.toLowerCase() as any);
setDrawerOpen(false);
}}
>
{tab.title}
</button>
))}
</div>
)}
<div className="flex gap-2">
<button
data-alt="login-button"
className="flex-1 h-10 rounded-full border border-black/20 dark:border-white/20"
onClick={() => router.push('/login')}
>
Log in
</button>
<button
data-alt="signup-button-secondary"
className="flex-1 h-10 rounded-full bg-black text-white hover:bg-black/90"
onClick={() => router.push('/signup')}
>
Sign up
</button>
</div>
</div>
)}
</div>
{/* 底部操作 */}
{isLogin && (
<div data-alt="drawer-footer" className="p-4 border-t border-black/10 dark:border-white/10">
<button
data-alt="logout-button"
className="w-full h-10 rounded-full border border-black/20 dark:border-white/20 flex items-center justify-center gap-2"
onClick={() => logoutUser()}
>
<LogOut className="h-4 w-4" />
Logout
</button>
</div>
)}
</div>
</Drawer>
{/* Sign-in Modal */}
<Dialog open={isSigninModalOpen} onOpenChange={(open) => {
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(()=>{}); }
}
}}>
<DialogContent
className="max-w-md mx-auto bg-white/95 dark:bg-gray-900/95 backdrop-blur-xl border-0 shadow-2xl"
data-alt="signin-modal"
>
<DialogTitle></DialogTitle>
<SigninBox onSuccess={async () => {
try {
if (user?.id) {
const res = await getUserSubscriptionInfo(String(user.id));
if (res?.data?.credits !== undefined) {
setCredits(res.data.credits);
}
}
} catch {}
// 成功签到后去除红点
setNeedsSigninBadge(false);
}} />
</DialogContent>
</Dialog>
</div>
</div>
</div>
</>
);
}

View File

@ -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 (
<div className="min-h-screen bg-background">
{isMobile || isTablet ? (
<H5TopBar />
) : (
<TopBar collapsed={sidebarCollapsed} isDesktop={isDesktop} />
)}
{isDesktop && <Sidebar collapsed={sidebarCollapsed} onToggle={setSidebarCollapsed} />}
<div
className={`top-[4rem] fixed right-0 bottom-0 px-4 ${getMobileContainerClasses()}`}
className={`fixed right-0 bottom-0 px-4 ${getMobileContainerClasses()}`}
style={{
...getLayoutStyles(),
top: (isMobile || isTablet) ? '3.5rem' : '4rem',
// 移动端使用动态高度计算
height: (isMobile || isTablet)
? 'calc(100dvh - 4rem)'
? 'calc(100dvh - 3.5rem)'
: 'calc(100vh - 4rem)'
}}
>

View File

@ -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();

View File

@ -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<SigninData>({
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)

22
components/layout/type.ts Normal file
View File

@ -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 },
],
}
];

View File

@ -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() {
</button>
))}
</div>
{/* 移动端开关移至 TopBar保留占位对齐 */}
{/* 移动端交由 H5TopBar 控制 */}
<span className="md:hidden" data-alt="mobile-menu-toggle-placeholder"></span>
</div>
{/* 移动端下拉(仅三个项) */}
{menuOpen && (
<div data-alt="mobile-menu" className="md:hidden bg-black/80 backdrop-blur-md border-b border-white/10 px-4 py-2 text-white/90 text-sm">
<div className="grid grid-cols-1 gap-1">
{tabsToRender.map((tab) => (
<button
key={tab.title}
data-alt={`m-nav-${tab.title.toLowerCase()}`}
className="text-center py-2"
onClick={() => scrollToSection(tab.title.toLowerCase() as any)}
>
{tab.title}
</button>
))}
</div>
</div>
)}
</div>
</div>
);
@ -268,7 +252,9 @@ export function HomePage2() {
return (
<div className="w-full h-screen overflow-y-auto" id="home-page" ref={containerRef} style={{ paddingBottom: `2rem` }}>
<NavBar />
{/* 移动端顶部导航(抽屉式) */}
<H5TopBar onSelectHomeTab={(key) => scrollToSection(key as any)} />
{/* <NavBar /> */}
<HomeModule1 />
<LazyLoad once>
<HomeModule2 />

View File

@ -191,9 +191,9 @@ const UsageView: React.FC = () => {
}, []);
return (
<div data-alt="usage-view-container" className="mx-auto max-w-5xl p-6">
<div data-alt="usage-view-container" className="mx-auto max-w-5xl p-4 sm:p-6">
<div data-alt="header" className="mb-6 flex items-center justify-between">
<h2 className="text-xl font-semibold text-white">Credit Usage Details</h2>
<h2 className="text-base sm:text-xl font-semibold text-white">Credit Usage Details</h2>
<div data-alt="period-switch" className="inline-flex rounded-lg bg-white/5 p-1">
{([7, 30, 90] as PeriodDays[]).map((d) => (
<button
@ -202,7 +202,7 @@ const UsageView: React.FC = () => {
data-alt={`period-${d}`}
onClick={() => handleChangeDays(d)}
className={
`px-3 py-1.5 text-sm rounded-md transition-colors ` +
`px-2 py-1 text-xs sm:px-3 sm:py-1.5 sm:text-sm rounded-md transition-colors ` +
(days === d
? "bg-[#C039F6] text-white"
: "text-white/80 hover:bg-white/10")
@ -214,32 +214,32 @@ const UsageView: React.FC = () => {
</div>
</div>
<div data-alt="meta" className="mb-3 text-sm text-white/70">
<div data-alt="meta" className="mb-3 text-xs sm:text-sm text-white/70">
<span data-alt="meta-period">Period: {periodLabel || "-"} days</span>
</div>
<div data-alt="table-wrapper" className="overflow-hidden rounded-lg border border-white/10">
<table data-alt="table" className="min-w-full table-fixed">
<div data-alt="table-wrapper" className="overflow-hidden overflow-x-auto rounded-lg border border-white/10">
<table data-alt="table" className="min-w-[32rem] sm:min-w-full table-auto">
<thead className="bg-white/5">
<tr>
<th className="w-1/4 px-4 py-2 text-left text-sm font-medium text-white">Kind</th>
<th className="w-1/4 px-4 py-2 text-right text-sm font-medium text-white">Credits</th>
<th className="w-1/4 px-4 py-2 text-left text-sm font-medium text-white">From</th>
<th className="w-1/4 px-4 py-2 text-left text-sm font-medium text-white">Date</th>
<th className="px-2 py-1 text-left text-xs sm:px-4 sm:py-2 sm:text-sm font-medium text-white">Kind</th>
<th className="px-2 py-1 text-left text-xs sm:px-4 sm:py-2 sm:text-sm font-medium text-white">Credits</th>
<th className="px-2 py-1 text-left text-xs sm:px-4 sm:py-2 sm:text-sm font-medium text-white">From</th>
<th className="px-2 py-1 text-left text-xs sm:px-4 sm:py-2 sm:text-sm font-medium text-white">Date</th>
</tr>
</thead>
<tbody className="divide-y divide-white/10">
{loading ? (
<tr>
<td className="px-4 py-3 text-white/70" colSpan={4} data-alt="row-loading">Loading...</td>
<td className="px-2 py-2 text-xs sm:px-4 sm:py-3 sm:text-sm text-white/70" colSpan={4} data-alt="row-loading">Loading...</td>
</tr>
) : error ? (
<tr>
<td className="px-4 py-3 text-white/70" data-alt="row-error" colSpan={4}>-</td>
<td className="px-2 py-2 text-xs sm:px-4 sm:py-3 sm:text-sm text-white/70" data-alt="row-error" colSpan={4}>-</td>
</tr>
) : items.length === 0 ? (
<tr>
<td className="px-4 py-3 text-white/70" data-alt="row-empty" colSpan={4}>-</td>
<td className="px-2 py-2 text-xs sm:px-4 sm:py-3 sm:text-sm text-white/70" data-alt="row-empty" colSpan={4}>-</td>
</tr>
) : (
items.map((it, idx) => {
@ -248,7 +248,7 @@ const UsageView: React.FC = () => {
return (
<React.Fragment key={key}>
<tr>
<td className="px-4 py-2 text-white/90" data-alt="cell-transaction-type">
<td className="px-2 py-1 text-xs sm:px-4 sm:py-2 sm:text-sm text-white/90" data-alt="cell-transaction-type">
<div data-alt="type-cell" className="flex items-center gap-2">
<span data-alt="type-text">{it?.transaction_type || '-'}</span>
{it?.project_info ? (
@ -272,47 +272,47 @@ const UsageView: React.FC = () => {
) : null}
</div>
</td>
<td className={`px-4 py-2 text-right ${getAmountColorClass(it?.change_type)}`} data-alt="cell-amount">
<td className={`px-2 py-1 text-left text-xs sm:px-4 sm:py-2 sm:text-sm ${getAmountColorClass(it?.change_type)}`} data-alt="cell-amount">
{Number.isFinite(it?.amount as number)
? `${it.change_type === 'INCREASE' ? '+' : it.change_type === 'DECREASE' ? '-' : ''}${it.amount}`
: '-'}
</td>
<td className="px-4 py-2 text-white/90" data-alt="cell-source-type">
<td className="px-2 py-1 text-xs sm:px-4 sm:py-2 sm:text-sm text-white/90" data-alt="cell-source-type">
{formatSource(it?.source_type)}
</td>
<td className="px-4 py-2 text-white/90" data-alt="cell-created-at">
<td className="px-2 py-1 text-xs sm:px-4 sm:py-2 sm:text-sm text-white/90" data-alt="cell-created-at">
{it?.created_at ? dateFormatter.format(new Date(it.created_at)) : '-'}
</td>
</tr>
{isExpanded && it.project_info && (
<tr data-alt="row-details">
<td colSpan={4} className="bg-white/5 px-4 py-3">
<div data-alt="project-summary" className="mb-2 text-sm text-white/90">
<td colSpan={4} className="bg-white/5 px-3 py-2 sm:px-4 sm:py-3">
<div data-alt="project-summary" className="mb-2 text-xs sm:text-sm text-white/90">
<div data-alt="project-name">Project: {it.project_info.project_name || '-'}</div>
<div data-alt="project-id" className="text-white/60">ID: {it.project_info.project_id || '-'}</div>
</div>
<div data-alt="videos-table-wrapper" className="overflow-hidden rounded-md border border-white/10">
<table data-alt="videos-table" className="min-w-full table-fixed">
<table data-alt="videos-table" className="min-w-[28rem] sm:min-w-full table-auto">
<tbody className="divide-y divide-white/10">
{(it.project_info.videos || []).length === 0 ? (
<tr>
<td className="px-3 py-2 text-white/70" colSpan={4} data-alt="videos-empty">-</td>
<td className="px-3 py-2 text-xs sm:text-sm text-white/70" colSpan={4} data-alt="videos-empty">-</td>
</tr>
) : (
it.project_info.videos.map((v, vIdx) => (
<tr key={`${v.created_at}-${vIdx}`}>
<td className="w-1/4 px-3 py-2 text-white/90" data-alt="video-name">
<td className="px-2 py-1 text-xs sm:px-3 sm:py-2 sm:text-sm text-white/90" data-alt="video-name">
{v.video_name && v.video_name.trim() ? v.video_name : `video${vIdx + 1}`}
</td>
<td className={`w-1/4 px-3 py-2 text-right ${getAmountColorClass(v.change_type)}`} data-alt="video-amount">
<td className={`px-2 py-1 text-left text-xs sm:px-3 sm:py-2 sm:text-sm ${getAmountColorClass(v.change_type)}`} data-alt="video-amount">
{Number.isFinite(v.amount as number)
? `${v.change_type === 'INCREASE' ? '+' : v.change_type === 'DECREASE' ? '-' : ''}${v.amount}`
: '-'}
</td>
<td className="w-1/4 px-3 py-2 text-white/90" data-alt="video-source">
<td className="px-2 py-1 text-xs sm:px-3 sm:py-2 sm:text-sm text-white/90" data-alt="video-source">
{formatSource(v.source_type)}
</td>
<td className="w-1/4 px-3 py-2 text-white/90" data-alt="video-created-at">
<td className="px-2 py-1 text-xs sm:px-3 sm:py-2 sm:text-sm text-white/90" data-alt="video-created-at">
{v.created_at ? dateFormatter.format(new Date(v.created_at)) : '-'}
</td>
</tr>
@ -332,7 +332,7 @@ const UsageView: React.FC = () => {
</table>
</div>
<div data-alt="pagination" className="mt-4 flex items-center justify-between text-sm text-white/80">
<div data-alt="pagination" className="mt-4 flex items-center justify-between text-xs sm:text-sm text-white/80">
<div data-alt="total-info">
Total {Number.isFinite(total) ? total : 0}
</div>
@ -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
</button>
<span data-alt="page-indicator" className="px-1 py-1.5">Page {page}</span>
<span data-alt="page-indicator" className="px-1 py-1 sm:py-1.5">Page {page}</span>
<button
type="button"
onClick={handleNext}
disabled={!canNext}
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 " +
(canNext ? "bg-white/10 hover:bg-white/20" : "bg-white/5 text-white/40 cursor-not-allowed")
}
data-alt="next-page"