's'
This commit is contained in:
@@ -25,7 +25,7 @@ export function AssetStats() {
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className={styles.card}
|
||||
className={`${styles.card} ${styles.landingStatCard}`}
|
||||
style={{
|
||||
textAlign: "center",
|
||||
animationDelay: `${0.25 + i * 0.05}s`,
|
||||
|
||||
@@ -4,54 +4,75 @@ import { useState } from "react";
|
||||
import { FAQ_ITEMS } from "@/app/data/vip.mock";
|
||||
import styles from "../vip.module.css";
|
||||
|
||||
function ChevronDown() {
|
||||
return (
|
||||
<svg className={styles.faqChevron} width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path
|
||||
d="M6 9l6 6 6-6"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function FAQSection() {
|
||||
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
||||
const [openIndex, setOpenIndex] = useState<number | null>(0);
|
||||
|
||||
return (
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>常见问题</h2>
|
||||
<div
|
||||
className="animate__animated animate__fadeInUp"
|
||||
style={{ animationDelay: "0.2s", animationFillMode: "both" }}
|
||||
>
|
||||
{FAQ_ITEMS.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={styles.card}
|
||||
style={{
|
||||
marginBottom: "0.75rem",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => setOpenIndex(openIndex === i ? null : i)}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
fontWeight: 500,
|
||||
color: "var(--foreground)",
|
||||
}}
|
||||
>
|
||||
{item.q}
|
||||
<span style={{ fontSize: "1.2rem", transition: "transform 0.2s", transform: openIndex === i ? "rotate(180deg)" : "none" }}>
|
||||
▼
|
||||
</span>
|
||||
</div>
|
||||
{openIndex === i && (
|
||||
<p
|
||||
style={{
|
||||
marginTop: "0.75rem",
|
||||
fontSize: "0.9rem",
|
||||
color: "var(--muted-foreground)",
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
<section className={`${styles.section} ${styles.faqSection}`} aria-labelledby="faq-heading">
|
||||
<div className={`${styles.faqInner} animate__animated animate__fadeInUp`} style={{ animationDelay: "0.15s", animationFillMode: "both" }}>
|
||||
<p className={styles.faqEyebrow}>
|
||||
<span className={styles.faqEyebrowDot} aria-hidden />
|
||||
FAQ
|
||||
</p>
|
||||
<h2 id="faq-heading" className={styles.faqHeadline}>
|
||||
常见问题
|
||||
</h2>
|
||||
<p className={styles.faqSub}>
|
||||
购买前可快速了解会员权益与流程;点击问题展开详情。
|
||||
</p>
|
||||
<div className={styles.faqList} role="list">
|
||||
{FAQ_ITEMS.map((item, i) => {
|
||||
const open = openIndex === i;
|
||||
const panelId = `faq-panel-${i}`;
|
||||
const triggerId = `faq-trigger-${i}`;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className={`${styles.faqItem} ${open ? styles.faqItemOpen : ""}`}
|
||||
role="listitem"
|
||||
>
|
||||
{item.a}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
id={triggerId}
|
||||
className={styles.faqTrigger}
|
||||
aria-expanded={open}
|
||||
aria-controls={panelId}
|
||||
onClick={() => setOpenIndex(open ? null : i)}
|
||||
>
|
||||
<span className={styles.faqIndex} aria-hidden>
|
||||
{i + 1}
|
||||
</span>
|
||||
<span className={styles.faqQuestionText}>{item.q}</span>
|
||||
<ChevronDown />
|
||||
</button>
|
||||
<div
|
||||
id={panelId}
|
||||
role="region"
|
||||
aria-labelledby={triggerId}
|
||||
className={`${styles.faqPanel} ${open ? styles.faqPanelOpen : ""}`}
|
||||
>
|
||||
<div className={styles.faqPanelInner}>
|
||||
<p className={styles.faqAnswer}>{item.a}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,9 @@ type LandingCTAProps = {
|
||||
onLogin: () => void;
|
||||
};
|
||||
|
||||
/** 设为 true 可重新展示「适合你 / 请止步」区块(下方 JSX 与 vip.module.css 内样式均保留) */
|
||||
const SHOW_CTA_AUDIENCE_BOUNDARY = false;
|
||||
|
||||
export function LandingCTA({ onJoin, onLogin }: LandingCTAProps) {
|
||||
const [restoreDate, setRestoreDate] = useState<string>("");
|
||||
useEffect(() => {
|
||||
@@ -16,50 +19,68 @@ export function LandingCTA({ onJoin, onLogin }: LandingCTAProps) {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className={styles.section} style={{ paddingBottom: "2rem" }}>
|
||||
<div
|
||||
className={`${styles.card} animate__animated animate__zoomIn`}
|
||||
style={{
|
||||
maxWidth: "560px",
|
||||
margin: "0 auto",
|
||||
padding: "1.5rem",
|
||||
textAlign: "center",
|
||||
animationDelay: "0.2s",
|
||||
animationFillMode: "both",
|
||||
}}
|
||||
>
|
||||
<h2 style={{ fontSize: "1.35rem", margin: "0 0 0.5rem", color: "var(--foreground)" }}>
|
||||
限时特惠 · {restoreDate || "—"}恢复原价
|
||||
</h2>
|
||||
<p style={{ fontSize: "0.9rem", color: "var(--muted-foreground)", margin: "0 0 1rem" }}>
|
||||
阶梯定价 · 数字交付 · 不支持退款
|
||||
</p>
|
||||
<p style={{ fontSize: "0.8rem", color: "var(--muted-foreground)", marginBottom: "1.5rem" }}>
|
||||
🚫 不适合躺赚、现金流紧张
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: "1rem",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onJoin}
|
||||
className={`${styles.btnPrimary} ${styles.btnLarge}`}
|
||||
<section className={`${styles.section} ${styles.ctaSection}`} aria-labelledby="cta-heading">
|
||||
<div className={`${styles.ctaInner} animate__animated animate__zoomIn`} style={{ animationDelay: "0.15s", animationFillMode: "both" }}>
|
||||
<div className={styles.ctaGlow} aria-hidden />
|
||||
<div className={styles.ctaCard}>
|
||||
<h2 id="cta-heading" className={styles.ctaPromoLine}>
|
||||
<span className={styles.ctaBadgeInline}>
|
||||
<span className={styles.ctaBadgePulse} aria-hidden />
|
||||
限时特惠
|
||||
</span>
|
||||
<span className={styles.ctaPromoDivider} aria-hidden>
|
||||
·
|
||||
</span>
|
||||
<span className={styles.ctaPromoMain}>
|
||||
<span className={styles.ctaHighlight}>{restoreDate || "—"}</span>
|
||||
<span className={styles.ctaPromoSuffix}>恢复原价</span>
|
||||
</span>
|
||||
</h2>
|
||||
<div className={styles.ctaChips}>
|
||||
<span className={styles.ctaChip}>阶梯定价</span>
|
||||
<span className={styles.ctaChip}>数字交付</span>
|
||||
<span className={styles.ctaChip}>持续更新</span>
|
||||
<span className={styles.ctaChip}>不支持退款</span>
|
||||
</div>
|
||||
<div
|
||||
className={`${styles.ctaBoundary} ${SHOW_CTA_AUDIENCE_BOUNDARY ? "" : styles.ctaBoundaryHidden}`}
|
||||
role="note"
|
||||
aria-hidden={!SHOW_CTA_AUDIENCE_BOUNDARY}
|
||||
>
|
||||
🔒 立即加入
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onLogin}
|
||||
className={styles.btnSecondary}
|
||||
>
|
||||
已有账号登录
|
||||
</button>
|
||||
<div className={styles.ctaBoundarySheen} aria-hidden />
|
||||
<div className={styles.ctaBoundaryInner}>
|
||||
<p className={styles.ctaBoundaryPos}>
|
||||
<span className={styles.ctaBoundaryMark} aria-hidden>
|
||||
✦
|
||||
</span>
|
||||
<span>
|
||||
<strong className={styles.ctaBoundaryStrong}>适合你:</strong>
|
||||
愿意投入行动、认真做增长与出海的同行者
|
||||
</span>
|
||||
</p>
|
||||
<p className={styles.ctaBoundaryNeg}>
|
||||
<span className={styles.ctaBoundaryMarkNeg} aria-hidden>
|
||||
·
|
||||
</span>
|
||||
<span>
|
||||
<strong className={styles.ctaBoundaryStrongNeg}>请止步:</strong>
|
||||
指望躺赚,或当下现金流极度紧张的朋友
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.ctaActions}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onJoin}
|
||||
className={`${styles.btnPrimary} ${styles.btnLarge} ${styles.ctaBtnPrimary}`}
|
||||
>
|
||||
🔒 立即加入
|
||||
</button>
|
||||
<button type="button" onClick={onLogin} className={styles.ctaBtnSecondary}>
|
||||
已有账号登录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -4,50 +4,23 @@ import styles from "../vip.module.css";
|
||||
|
||||
export function LandingHero() {
|
||||
return (
|
||||
<section className={`${styles.section} ${styles.heroSection}`} style={{ textAlign: "center" }}>
|
||||
<section className={`${styles.section} ${styles.heroSection} ${styles.heroSection3d}`} style={{ textAlign: "center" }}>
|
||||
<p
|
||||
className="animate__animated animate__fadeInDown"
|
||||
style={{
|
||||
display: "inline-block",
|
||||
fontSize: "0.875rem",
|
||||
color: "var(--muted-foreground)",
|
||||
background: "var(--card)",
|
||||
padding: "0.35rem 0.85rem",
|
||||
borderRadius: "9999px",
|
||||
border: "1px solid var(--border)",
|
||||
marginBottom: "1rem",
|
||||
animationDelay: "0.1s",
|
||||
animationFillMode: "both",
|
||||
}}
|
||||
className={`${styles.heroEyebrow} animate__animated animate__fadeInDown`}
|
||||
style={{ animationDelay: "0.1s", animationFillMode: "both" }}
|
||||
>
|
||||
异度星球会员中心
|
||||
</p>
|
||||
<h1
|
||||
className="animate__animated animate__fadeInDown"
|
||||
style={{
|
||||
fontSize: "clamp(2rem, 6vw, 3rem)",
|
||||
fontWeight: 700,
|
||||
margin: "0 0 0.5rem",
|
||||
letterSpacing: "-0.02em",
|
||||
lineHeight: 1.2,
|
||||
animationDelay: "0.2s",
|
||||
animationFillMode: "both",
|
||||
}}
|
||||
className={`${styles.heroTitle} animate__animated animate__fadeInDown`}
|
||||
style={{ animationDelay: "0.2s", animationFillMode: "both" }}
|
||||
>
|
||||
全模块解锁 · 持续更新 · 抱团出海
|
||||
<span className={styles.heroTitleAccent}>全模块解锁</span>
|
||||
<span className={styles.heroTitleRest}> · 持续更新 · 抱团出海</span>
|
||||
</h1>
|
||||
<p
|
||||
className="animate__animated animate__fadeInDown"
|
||||
style={{
|
||||
fontSize: "1.125rem",
|
||||
color: "var(--muted-foreground)",
|
||||
maxWidth: "540px",
|
||||
margin: "0 auto",
|
||||
lineHeight: 1.6,
|
||||
textAlign: "center",
|
||||
animationDelay: "0.3s",
|
||||
animationFillMode: "both",
|
||||
}}
|
||||
className={`${styles.heroLead} animate__animated animate__fadeInDown`}
|
||||
style={{ animationDelay: "0.3s", animationFillMode: "both" }}
|
||||
>
|
||||
一次付费,解锁电子书、视频教程、私密社群、即时问答,每周更新
|
||||
</p>
|
||||
|
||||
@@ -12,6 +12,8 @@ type LoginFormProps = {
|
||||
) => Promise<boolean | { ok: boolean; error?: string }>;
|
||||
onSuccess: () => void;
|
||||
autoFocus?: boolean;
|
||||
/** 验证请求进行中(用于全屏 loading 等) */
|
||||
onBusyChange?: (busy: boolean) => void;
|
||||
};
|
||||
|
||||
export function LoginForm({
|
||||
@@ -19,6 +21,7 @@ export function LoginForm({
|
||||
onVerifyByEmail,
|
||||
onSuccess,
|
||||
autoFocus,
|
||||
onBusyChange,
|
||||
}: LoginFormProps) {
|
||||
const [mode, setMode] = useState<"user_id" | "email">("user_id");
|
||||
const [userId, setUserId] = useState("");
|
||||
@@ -41,6 +44,10 @@ export function LoginForm({
|
||||
if (autoFocus) inputRef.current?.focus();
|
||||
}, [autoFocus, mode]);
|
||||
|
||||
useEffect(() => {
|
||||
onBusyChange?.(loading);
|
||||
}, [loading, onBusyChange]);
|
||||
|
||||
const handleUserIdSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
const trimmed = userId.trim();
|
||||
@@ -51,14 +58,19 @@ export function LoginForm({
|
||||
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
let succeeded = false;
|
||||
try {
|
||||
const ok = await onVerify(trimmed);
|
||||
if (ok) onSuccess();
|
||||
else setError("未找到该账号的会员记录,请确认账号正确");
|
||||
if (ok) {
|
||||
succeeded = true;
|
||||
onSuccess();
|
||||
} else {
|
||||
setError("未找到该账号的会员记录,请确认账号正确");
|
||||
}
|
||||
} catch {
|
||||
setError("验证失败,请稍后重试");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (!succeeded) setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -79,6 +91,7 @@ export function LoginForm({
|
||||
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
let succeeded = false;
|
||||
try {
|
||||
const candidateUserId = getCandidateUserId();
|
||||
const result = await onVerifyByEmail(
|
||||
@@ -88,12 +101,16 @@ export function LoginForm({
|
||||
);
|
||||
const ok = typeof result === "object" ? result.ok : result;
|
||||
const errMsg = typeof result === "object" ? result.error : undefined;
|
||||
if (ok) onSuccess();
|
||||
else setError(errMsg || "邮箱或密码错误,或未找到关联的会员记录");
|
||||
if (ok) {
|
||||
succeeded = true;
|
||||
onSuccess();
|
||||
} else {
|
||||
setError(errMsg || "邮箱或密码错误,或未找到关联的会员记录");
|
||||
}
|
||||
} catch {
|
||||
setError("验证失败,请稍后重试");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (!succeeded) setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { HomeBootOverlay } from "@/app/components/HomeBootOverlay";
|
||||
import styles from "../vip.module.css";
|
||||
import { LoginForm } from "./LoginForm";
|
||||
|
||||
@@ -17,11 +18,14 @@ type LoginModalProps = {
|
||||
};
|
||||
|
||||
export function LoginModal({ open, onClose, onVerify, onVerifyByEmail, onSuccess }: LoginModalProps) {
|
||||
const [verifyOverlay, setVerifyOverlay] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
document.body.style.overflow = "hidden";
|
||||
} else {
|
||||
document.body.style.overflow = "";
|
||||
setVerifyOverlay(false);
|
||||
}
|
||||
return () => {
|
||||
document.body.style.overflow = "";
|
||||
@@ -31,56 +35,63 @@ export function LoginModal({ open, onClose, onVerify, onVerifyByEmail, onSuccess
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") onClose();
|
||||
if (e.key === "Escape" && !verifyOverlay) onClose();
|
||||
};
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
return () => window.removeEventListener("keydown", onKeyDown);
|
||||
}, [open, onClose]);
|
||||
}, [open, onClose, verifyOverlay]);
|
||||
|
||||
const handleSuccess = () => {
|
||||
onClose();
|
||||
// 勿先 onClose:父级会把 open 设为 false,弹窗卸载会清掉全屏 loading,应在 reload 前保持验证中态
|
||||
onSuccess();
|
||||
};
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="login-modal-title"
|
||||
>
|
||||
<>
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
/>
|
||||
<div
|
||||
className="relative w-full max-w-[400px] rounded-2xl border border-[var(--border)] bg-[var(--card)] p-6 shadow-2xl animate__animated animate__fadeIn animate__faster"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="login-modal-title"
|
||||
>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 id="login-modal-title" className={styles.sectionTitle} style={{ margin: 0 }}>
|
||||
登录
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="rounded-lg p-2 text-[var(--muted-foreground)] transition hover:bg-[var(--muted)] hover:text-[var(--foreground)]"
|
||||
aria-label="关闭"
|
||||
>
|
||||
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<LoginForm
|
||||
onVerify={onVerify}
|
||||
onVerifyByEmail={onVerifyByEmail}
|
||||
onSuccess={handleSuccess}
|
||||
autoFocus
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||
onClick={verifyOverlay ? undefined : onClose}
|
||||
/>
|
||||
<div
|
||||
className="relative w-full max-w-[400px] rounded-2xl border border-[var(--border)] bg-[var(--card)] p-6 shadow-2xl animate__animated animate__fadeIn animate__faster"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 id="login-modal-title" className={styles.sectionTitle} style={{ margin: 0 }}>
|
||||
登录
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
disabled={verifyOverlay}
|
||||
className="rounded-lg p-2 text-[var(--muted-foreground)] transition hover:bg-[var(--muted)] hover:text-[var(--foreground)] disabled:pointer-events-none disabled:opacity-40"
|
||||
aria-label="关闭"
|
||||
>
|
||||
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<LoginForm
|
||||
onVerify={onVerify}
|
||||
onVerifyByEmail={onVerifyByEmail}
|
||||
onSuccess={handleSuccess}
|
||||
onBusyChange={setVerifyOverlay}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{verifyOverlay ? (
|
||||
<HomeBootOverlay visible subtitle="正在验证并解锁会员…" />
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,9 +7,14 @@ import { NetdiskDownloadsSection } from "@/app/components/NetdiskDownloadsSectio
|
||||
|
||||
type TopicEbookShowcaseProps = {
|
||||
onLockedClick?: () => void;
|
||||
/** VIP 首页:点击「私密社群」打开社群登录信息弹窗 */
|
||||
onCommunityCardClick?: () => void;
|
||||
};
|
||||
|
||||
export function TopicEbookShowcase({ onLockedClick }: TopicEbookShowcaseProps) {
|
||||
export function TopicEbookShowcase({
|
||||
onLockedClick,
|
||||
onCommunityCardClick,
|
||||
}: TopicEbookShowcaseProps) {
|
||||
return (
|
||||
<section id="topics" className={`${styles.section} ${styles.topicsSection}`}>
|
||||
{/* 模块展示 */}
|
||||
@@ -21,6 +26,7 @@ export function TopicEbookShowcase({ onLockedClick }: TopicEbookShowcaseProps) {
|
||||
{TOPICS_DISPLAY.map((t) => {
|
||||
const { id, href } = t;
|
||||
const isLocked = "locked" in t && t.locked && onLockedClick;
|
||||
const isCommunityModal = id === "community" && onCommunityCardClick;
|
||||
const content = (
|
||||
<span className={styles.topicCardInner}>
|
||||
{isLocked && (
|
||||
@@ -44,6 +50,19 @@ export function TopicEbookShowcase({ onLockedClick }: TopicEbookShowcaseProps) {
|
||||
</button>
|
||||
);
|
||||
}
|
||||
if (isCommunityModal) {
|
||||
return (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
className={styles.topicCard}
|
||||
onClick={onCommunityCardClick}
|
||||
style={{ cursor: "pointer", border: "none", font: "inherit" }}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
if (href) {
|
||||
return (
|
||||
<Link
|
||||
|
||||
Reference in New Issue
Block a user