This commit is contained in:
eric
2026-03-31 12:44:54 -05:00
parent 2ffbddff2e
commit 4497604242
10 changed files with 359 additions and 28 deletions

View File

@@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useRef, useState } from "react";
import { LandingHero } from "./landing/LandingHero";
import { AssetStats } from "./landing/AssetStats";
import { TopicEbookShowcase } from "./landing/TopicEbookShowcase";
@@ -31,6 +31,22 @@ export function LandingPage({
}: LandingPageProps) {
const [loginModalOpen, setLoginModalOpen] = useState(false);
const [promoModalOpen, setPromoModalOpen] = useState(false);
const [payTestMode, setPayTestMode] = useState(() => {
if (typeof window === "undefined") return false;
return localStorage.getItem("vipPayTestMode") === "1";
});
const titleTapCountRef = useRef(0);
const handleHeroTitleTap = () => {
titleTapCountRef.current += 1;
if (titleTapCountRef.current < 10) return;
titleTapCountRef.current = 0;
const nextMode = !payTestMode;
setPayTestMode(nextMode);
if (typeof window !== "undefined") {
localStorage.setItem("vipPayTestMode", nextMode ? "1" : "0");
}
};
return (
<>
@@ -43,7 +59,7 @@ export function LandingPage({
<div className={styles.landingNoise} />
</div>
<div className={styles.landingSceneInner}>
<LandingHero />
<LandingHero onTitleTap={handleHeroTitleTap} />
<AssetStats />
<TopicEbookShowcase onLockedClick={() => setLoginModalOpen(true)} />
<CompareSection />
@@ -54,6 +70,7 @@ export function LandingPage({
<LandingCTA
onJoin={onJoin}
onLogin={() => setLoginModalOpen(true)}
payTestMode={payTestMode}
/>
</div>
</div>

View File

@@ -17,10 +17,19 @@ type VipHeaderProps = {
isLoggedIn: boolean;
userName?: string | null;
userEmail?: string | null;
wechatNick?: string | null;
wechatAvatar?: string | null;
onLogout?: () => void;
};
export function VipHeader({ isLoggedIn, userName, userEmail, onLogout }: VipHeaderProps) {
export function VipHeader({
isLoggedIn,
userName,
userEmail,
wechatNick,
wechatAvatar,
onLogout,
}: VipHeaderProps) {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
@@ -38,8 +47,9 @@ export function VipHeader({ isLoggedIn, userName, userEmail, onLogout }: VipHead
onLogout?.();
};
const showUser = isLoggedIn || userEmail;
const displayName = userName || userEmail || "会员";
const hasWechatProfile = !!(wechatNick || wechatAvatar);
const showUser = isLoggedIn || !!userEmail || hasWechatProfile;
const displayName = wechatNick || userName || userEmail || "会员";
return (
<header className={styles.header}>
@@ -54,10 +64,15 @@ export function VipHeader({ isLoggedIn, userName, userEmail, onLogout }: VipHead
<button
type="button"
onClick={() => setOpen((o) => !o)}
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-amber-500 text-sm font-semibold text-white shadow-sm hover:bg-amber-600"
className="flex h-9 w-9 shrink-0 items-center justify-center overflow-hidden rounded-full bg-amber-500 text-sm font-semibold text-white shadow-sm hover:bg-amber-600"
aria-label="用户菜单"
>
{getAvatarLetter(displayName)}
{wechatAvatar ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={wechatAvatar} alt={displayName} className="h-full w-full object-cover" />
) : (
getAvatarLetter(displayName)
)}
</button>
{isLoggedIn && (
<span

View File

@@ -9,13 +9,30 @@ type VipLayoutProps = {
isLoggedIn: boolean;
userName?: string | null;
userEmail?: string | null;
wechatNick?: string | null;
wechatAvatar?: string | null;
onLogout?: () => void;
};
export function VipLayout({ children, isLoggedIn, userName, userEmail, onLogout }: VipLayoutProps) {
export function VipLayout({
children,
isLoggedIn,
userName,
userEmail,
wechatNick,
wechatAvatar,
onLogout,
}: VipLayoutProps) {
return (
<div className={styles.root}>
<VipHeader isLoggedIn={isLoggedIn} userName={userName} userEmail={userEmail} onLogout={onLogout} />
<VipHeader
isLoggedIn={isLoggedIn}
userName={userName}
userEmail={userEmail}
wechatNick={wechatNick}
wechatAvatar={wechatAvatar}
onLogout={onLogout}
/>
<main className={styles.main}>{children}</main>
</div>
);

View File

@@ -1,22 +1,20 @@
"use client";
import { useState, useEffect } from "react";
import { useState } from "react";
import { formatRestoreDateShort } from "@/config/promo.config";
import styles from "../vip.module.css";
type LandingCTAProps = {
onJoin: () => void;
onLogin: () => void;
payTestMode?: boolean;
};
/** 设为 true 可重新展示「适合你 / 请止步」区块(下方 JSX 与 vip.module.css 内样式均保留) */
const SHOW_CTA_AUDIENCE_BOUNDARY = false;
export function LandingCTA({ onJoin, onLogin }: LandingCTAProps) {
const [restoreDate, setRestoreDate] = useState<string>("");
useEffect(() => {
setRestoreDate(formatRestoreDateShort());
}, []);
export function LandingCTA({ onJoin, onLogin, payTestMode }: LandingCTAProps) {
const [restoreDate] = useState<string>(() => formatRestoreDateShort());
return (
<section className={`${styles.section} ${styles.ctaSection}`} aria-labelledby="cta-heading">
@@ -74,6 +72,7 @@ export function LandingCTA({ onJoin, onLogin }: LandingCTAProps) {
type="button"
onClick={onJoin}
className={`${styles.btnPrimary} ${styles.btnLarge} ${styles.ctaBtnPrimary}`}
style={payTestMode ? { background: "linear-gradient(135deg,#16a34a,#22c55e)" } : undefined}
>
🔒 立即加入
</button>

View File

@@ -2,12 +2,18 @@
import styles from "../vip.module.css";
export function LandingHero() {
type LandingHeroProps = {
onTitleTap?: () => void;
};
export function LandingHero({ onTitleTap }: LandingHeroProps) {
return (
<section className={`${styles.section} ${styles.heroSection} ${styles.heroSection3d}`} style={{ textAlign: "center" }}>
<p
className={`${styles.heroEyebrow} animate__animated animate__fadeInDown`}
style={{ animationDelay: "0.1s", animationFillMode: "both" }}
style={{ animationDelay: "0.1s", animationFillMode: "both", cursor: "pointer" }}
onClick={onTitleTap}
title="连续点击 10 次切换测试价格"
>
异度星球会员中心
</p>