's'
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { WelcomeBlock } from "./dashboard/WelcomeBlock";
|
||||
import { CommunityAccessModal } from "./dashboard/CommunityAccessModal";
|
||||
import { UnlockedStats } from "./dashboard/UnlockedStats";
|
||||
import { TopicEbookShowcase } from "./landing/TopicEbookShowcase";
|
||||
|
||||
@@ -31,6 +33,7 @@ function requiresEmailBinding(): boolean {
|
||||
|
||||
export function DashboardPage({ userName, userEmail }: DashboardPageProps) {
|
||||
const bindingRequired = requiresEmailBinding();
|
||||
const [communityModalOpen, setCommunityModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -40,7 +43,11 @@ export function DashboardPage({ userName, userEmail }: DashboardPageProps) {
|
||||
bindingRequired={bindingRequired}
|
||||
/>
|
||||
<UnlockedStats />
|
||||
<TopicEbookShowcase />
|
||||
<TopicEbookShowcase onCommunityCardClick={() => setCommunityModalOpen(true)} />
|
||||
<CommunityAccessModal
|
||||
open={communityModalOpen}
|
||||
onClose={() => setCommunityModalOpen(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { CommunityPreview } from "./landing/CommunityPreview";
|
||||
import { FAQSection } from "./landing/FAQSection";
|
||||
import { LandingCTA } from "./landing/LandingCTA";
|
||||
import { LoginModal } from "./landing/LoginModal";
|
||||
import styles from "./vip.module.css";
|
||||
|
||||
type LandingPageProps = {
|
||||
onJoin: () => void;
|
||||
@@ -31,13 +32,26 @@ export function LandingPage({
|
||||
|
||||
return (
|
||||
<>
|
||||
<LandingHero />
|
||||
<AssetStats />
|
||||
<TopicEbookShowcase onLockedClick={() => setLoginModalOpen(true)} />
|
||||
<CompareSection />
|
||||
<CommunityPreview />
|
||||
<FAQSection />
|
||||
<LandingCTA onJoin={onJoin} onLogin={() => setLoginModalOpen(true)} />
|
||||
<div className={styles.landingScene}>
|
||||
<div className={styles.landingSceneBg} aria-hidden>
|
||||
<div className={styles.landingOrb} />
|
||||
<div className={styles.landingOrb} />
|
||||
<div className={styles.landingOrb} />
|
||||
<div className={styles.landingMesh} />
|
||||
<div className={styles.landingNoise} />
|
||||
</div>
|
||||
<div className={styles.landingSceneInner}>
|
||||
<LandingHero />
|
||||
<AssetStats />
|
||||
<TopicEbookShowcase onLockedClick={() => setLoginModalOpen(true)} />
|
||||
<CompareSection />
|
||||
<div className="hidden" aria-hidden>
|
||||
<CommunityPreview />
|
||||
</div>
|
||||
<FAQSection />
|
||||
<LandingCTA onJoin={onJoin} onLogin={() => setLoginModalOpen(true)} />
|
||||
</div>
|
||||
</div>
|
||||
<LoginModal
|
||||
open={loginModalOpen}
|
||||
onClose={() => setLoginModalOpen(false)}
|
||||
|
||||
@@ -43,12 +43,7 @@ export function VipHeader({ isLoggedIn, userName, userEmail, onLogout }: VipHead
|
||||
|
||||
return (
|
||||
<header className={styles.header}>
|
||||
<Link
|
||||
href="https://digital.hackrobot.cn/zh"
|
||||
className={styles.logo}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<Link href="/" className={styles.logo}>
|
||||
🌍 异度星球
|
||||
</Link>
|
||||
<nav className={styles.nav}>
|
||||
|
||||
133
app/vip/components/dashboard/CommunityAccessModal.tsx
Normal file
133
app/vip/components/dashboard/CommunityAccessModal.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Copyable } from "../Copyable";
|
||||
import styles from "../vip.module.css";
|
||||
|
||||
const COMMUNITY_URL = "https://qun.hackrobot.cn";
|
||||
const DEFAULT_PASSWORD = "12345678";
|
||||
|
||||
function getStorage(key: string): string | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
return localStorage.getItem(key);
|
||||
}
|
||||
|
||||
function isAnonymousUserId(value: string | null): value is string {
|
||||
return !!value && /^user\d+$/.test(value);
|
||||
}
|
||||
|
||||
type CommunityAccessModalProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
/** 与 WelcomeBlock 同步的社群登录账号解析,用于弹窗展示 */
|
||||
export function CommunityAccessModal({ open, onClose }: CommunityAccessModalProps) {
|
||||
const userId = typeof window !== "undefined" ? getStorage("userId") : null;
|
||||
const [backendMemosAccount, setBackendMemosAccount] = useState<string | null>(null);
|
||||
|
||||
const memosAccount =
|
||||
getStorage("memosAccount") ||
|
||||
backendMemosAccount ||
|
||||
(isAnonymousUserId(userId) ? userId : null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !userId) return;
|
||||
fetch(`/api/vip/linked-email?user_id=${encodeURIComponent(userId)}`, {
|
||||
cache: "no-store",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
const linkedAnonymousUserId =
|
||||
typeof data?.anonymousUserId === "string" && /^user\d+$/.test(data.anonymousUserId)
|
||||
? data.anonymousUserId
|
||||
: null;
|
||||
if (linkedAnonymousUserId && typeof window !== "undefined") {
|
||||
setBackendMemosAccount(linkedAnonymousUserId);
|
||||
localStorage.setItem("memosAccount", linkedAnonymousUserId);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [open, userId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") onClose();
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, [open, onClose]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.communityModalBackdrop}
|
||||
role="presentation"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className={styles.communityModalPanel}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="community-modal-title"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className={styles.communityModalGlow} aria-hidden />
|
||||
<button
|
||||
type="button"
|
||||
className={styles.communityModalClose}
|
||||
onClick={onClose}
|
||||
aria-label="关闭"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<div className={styles.communityModalIconWrap}>
|
||||
<span className={styles.communityModalIcon}>👥</span>
|
||||
</div>
|
||||
<h2 id="community-modal-title" className={styles.communityModalTitle}>
|
||||
私密社群 · 登录信息
|
||||
</h2>
|
||||
<p className={styles.communityModalDesc}>
|
||||
使用以下信息访问社群站点,可随时点击复制
|
||||
</p>
|
||||
|
||||
<div className={styles.communityModalRows}>
|
||||
<div className={styles.communityModalRow}>
|
||||
<span className={styles.communityModalLabel}>网址</span>
|
||||
<div className={styles.communityModalValueWrap}>
|
||||
<Copyable text={COMMUNITY_URL} className={styles.communityModalCopyable}>
|
||||
<span className={styles.communityModalMono}>qun.hackrobot.cn</span>
|
||||
</Copyable>
|
||||
<a
|
||||
href={COMMUNITY_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.communityModalOpenLink}
|
||||
>
|
||||
打开
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.communityModalRow}>
|
||||
<span className={styles.communityModalLabel}>账号</span>
|
||||
<Copyable text={memosAccount || ""} className={styles.communityModalCopyable}>
|
||||
<span className={styles.communityModalMono}>{memosAccount || "—"}</span>
|
||||
</Copyable>
|
||||
</div>
|
||||
<div className={styles.communityModalRow}>
|
||||
<span className={styles.communityModalLabel}>密码</span>
|
||||
<Copyable text={DEFAULT_PASSWORD} className={styles.communityModalCopyable}>
|
||||
<span className={styles.communityModalMono}>{DEFAULT_PASSWORD}</span>
|
||||
</Copyable>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" className={styles.communityModalDone} onClick={onClose}>
|
||||
知道了
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -287,38 +287,12 @@ export function WelcomeBlock({
|
||||
>
|
||||
欢迎回来,<Copyable text={displayName}>{displayName}</Copyable>
|
||||
</h1>
|
||||
<p
|
||||
className="animate__animated animate__fadeInDown"
|
||||
style={{
|
||||
fontSize: "0.95rem",
|
||||
color: "var(--muted-foreground)",
|
||||
margin: "0.25rem 0 0",
|
||||
animationDelay: "0.15s",
|
||||
animationFillMode: "both",
|
||||
}}
|
||||
>
|
||||
以下是当前会员账号和登录信息
|
||||
</p>
|
||||
<div className={styles.loginCard}>
|
||||
<div className={styles.loginCardTitle}>会员登录信息</div>
|
||||
<div className={styles.loginCardRow}>
|
||||
<span className={styles.loginCardLabel}>网址</span>
|
||||
<Copyable text="https://qun.hackrobot.cn">
|
||||
qun.hackrobot.cn
|
||||
</Copyable>
|
||||
<a
|
||||
href="https://qun.hackrobot.cn"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.loginCardLink}
|
||||
>
|
||||
打开
|
||||
</a>
|
||||
</div>
|
||||
<div className={styles.loginCardRow}>
|
||||
<span className={styles.loginCardLabel}>账号</span>
|
||||
<Copyable text={memosAccount || ""}>
|
||||
<span className={styles.loginCardValue}>{memosAccount || "-"}</span>
|
||||
<span className={styles.loginCardLabel}>邮箱</span>
|
||||
<Copyable text={displayEmail || ""}>
|
||||
<span className={styles.loginCardValue}>{displayEmail || "-"}</span>
|
||||
</Copyable>
|
||||
</div>
|
||||
<div className={styles.loginCardRow}>
|
||||
@@ -327,12 +301,6 @@ export function WelcomeBlock({
|
||||
<span className={styles.loginCardValue}>{DEFAULT_PASSWORD}</span>
|
||||
</Copyable>
|
||||
</div>
|
||||
<div className={styles.loginCardRow}>
|
||||
<span className={styles.loginCardLabel}>邮箱</span>
|
||||
<Copyable text={displayEmail || ""}>
|
||||
<span className={styles.loginCardValue}>{displayEmail || "-"}</span>
|
||||
</Copyable>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -118,6 +118,233 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 首页 · 沉浸式背景(透视网格 / 光晕 · 轻 3D 感,无 WebGL 依赖) ========== */
|
||||
.landingScene {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
margin-left: -1.5rem;
|
||||
margin-right: -1.5rem;
|
||||
width: calc(100% + 3rem);
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.landingScene {
|
||||
margin-left: -1rem;
|
||||
margin-right: -1rem;
|
||||
width: calc(100% + 2rem);
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.landingSceneBg {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: min(118vh, 1400px);
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.landingSceneInner {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.landingOrb {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(52px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.landingOrb:nth-child(1) {
|
||||
width: min(42vw, 380px);
|
||||
height: min(42vw, 380px);
|
||||
top: -4%;
|
||||
left: -10%;
|
||||
background: radial-gradient(circle, rgba(245, 158, 11, 0.38) 0%, transparent 68%);
|
||||
animation: landingOrbA 22s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.landingOrb:nth-child(2) {
|
||||
width: min(48vw, 420px);
|
||||
height: min(48vw, 420px);
|
||||
top: 10%;
|
||||
right: -16%;
|
||||
background: radial-gradient(circle, rgba(245, 158, 11, 0.14) 0%, transparent 72%);
|
||||
animation: landingOrbB 26s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.landingOrb:nth-child(3) {
|
||||
width: min(34vw, 300px);
|
||||
height: min(34vw, 300px);
|
||||
top: 36%;
|
||||
left: 14%;
|
||||
background: radial-gradient(circle, rgba(251, 191, 36, 0.22) 0%, transparent 72%);
|
||||
animation: landingOrbC 19s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.dark .landingOrb:nth-child(2) {
|
||||
background: radial-gradient(circle, rgba(129, 140, 248, 0.22) 0%, transparent 72%);
|
||||
}
|
||||
|
||||
@keyframes landingOrbA {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
50% { transform: translate(5%, 5%) scale(1.06); }
|
||||
}
|
||||
|
||||
@keyframes landingOrbB {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
50% { transform: translate(-4%, 6%) scale(1.04); }
|
||||
}
|
||||
|
||||
@keyframes landingOrbC {
|
||||
0% { transform: translate(0, 0); }
|
||||
100% { transform: translate(4%, -3%); }
|
||||
}
|
||||
|
||||
.landingMesh {
|
||||
position: absolute;
|
||||
left: -20%;
|
||||
width: 140%;
|
||||
top: 0;
|
||||
height: min(64vh, 580px);
|
||||
pointer-events: none;
|
||||
perspective: 520px;
|
||||
opacity: 0.88;
|
||||
}
|
||||
|
||||
.landingMesh::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image:
|
||||
linear-gradient(rgba(245, 158, 11, 0.1) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(245, 158, 11, 0.1) 1px, transparent 1px);
|
||||
background-size: 52px 52px;
|
||||
transform-origin: 50% 0%;
|
||||
transform: rotateX(74deg) translateY(-5%) scale(1.06);
|
||||
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.62) 0%, transparent 92%);
|
||||
-webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.62) 0%, transparent 92%);
|
||||
animation: landingMeshScroll 28s linear infinite;
|
||||
}
|
||||
|
||||
.dark .landingMesh::before {
|
||||
background-image:
|
||||
linear-gradient(rgba(245, 158, 11, 0.15) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(245, 158, 11, 0.15) 1px, transparent 1px);
|
||||
}
|
||||
|
||||
@keyframes landingMeshScroll {
|
||||
from {
|
||||
background-position: 0 0, 0 0;
|
||||
}
|
||||
to {
|
||||
background-position: 0 52px, 52px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.landingNoise {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0.04;
|
||||
pointer-events: none;
|
||||
mix-blend-mode: overlay;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.dark .landingNoise {
|
||||
opacity: 0.08;
|
||||
mix-blend-mode: soft-light;
|
||||
}
|
||||
|
||||
.heroSection3d {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.heroEyebrow {
|
||||
display: inline-block;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
color: var(--muted-foreground);
|
||||
padding: 0.4rem 0.95rem;
|
||||
border-radius: 9999px;
|
||||
border: 1px solid rgba(245, 158, 11, 0.22);
|
||||
margin-bottom: 1rem;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
backdrop-filter: blur(12px) saturate(1.2);
|
||||
box-shadow:
|
||||
0 10px 36px rgba(0, 0, 0, 0.07),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.55) inset;
|
||||
}
|
||||
|
||||
.dark .heroEyebrow {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.06) inset;
|
||||
}
|
||||
|
||||
.heroTitle {
|
||||
font-size: clamp(2rem, 6vw, 3rem);
|
||||
font-weight: 700;
|
||||
margin: 0 0 0.5rem;
|
||||
letter-spacing: -0.03em;
|
||||
line-height: 1.18;
|
||||
}
|
||||
|
||||
.heroTitleAccent {
|
||||
background: linear-gradient(120deg, var(--foreground) 0%, var(--accent) 42%, #fbbf24 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.heroTitleRest {
|
||||
color: var(--foreground);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.heroLead {
|
||||
font-size: clamp(1rem, 2.8vw, 1.125rem);
|
||||
color: var(--muted-foreground);
|
||||
max-width: 540px;
|
||||
margin: 0 auto;
|
||||
line-height: 1.65;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.landingStatCard {
|
||||
position: relative;
|
||||
backdrop-filter: blur(12px) saturate(1.15);
|
||||
background: rgba(255, 255, 255, 0.52);
|
||||
border-color: rgba(245, 158, 11, 0.2);
|
||||
transition: transform 0.35s ease, box-shadow 0.35s ease, border-color 0.2s;
|
||||
}
|
||||
|
||||
.dark .landingStatCard {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-color: rgba(245, 158, 11, 0.24);
|
||||
}
|
||||
|
||||
.landingStatCard:hover {
|
||||
transform: translateY(-3px) perspective(880px) rotateX(5deg);
|
||||
box-shadow:
|
||||
0 18px 44px rgba(0, 0, 0, 0.1),
|
||||
0 0 0 1px rgba(245, 158, 11, 0.14);
|
||||
}
|
||||
|
||||
.dark .landingStatCard:hover {
|
||||
box-shadow:
|
||||
0 22px 52px rgba(0, 0, 0, 0.48),
|
||||
0 0 0 1px rgba(245, 158, 11, 0.22);
|
||||
}
|
||||
|
||||
.statsSection {
|
||||
padding-top: 2rem;
|
||||
padding-bottom: 2rem;
|
||||
@@ -426,3 +653,731 @@
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 私密社群 · 登录信息弹窗 */
|
||||
.communityModalBackdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
background: rgba(15, 23, 42, 0.65);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.communityModalPanel {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
padding: 1.75rem 1.5rem 1.5rem;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(245, 158, 11, 0.25);
|
||||
background: var(--card);
|
||||
box-shadow:
|
||||
0 24px 48px rgba(0, 0, 0, 0.35),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.04) inset;
|
||||
}
|
||||
|
||||
.communityModalGlow {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
inset: -1px;
|
||||
border-radius: 16px;
|
||||
background: radial-gradient(
|
||||
ellipse 80% 60% at 50% -20%,
|
||||
rgba(245, 158, 11, 0.18),
|
||||
transparent 55%
|
||||
);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.communityModalClose {
|
||||
position: absolute;
|
||||
top: 0.65rem;
|
||||
right: 0.65rem;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 1.35rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.communityModalClose:hover {
|
||||
color: var(--foreground);
|
||||
background: var(--muted);
|
||||
}
|
||||
|
||||
.communityModalIconWrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.communityModalIcon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 3.25rem;
|
||||
height: 3.25rem;
|
||||
border-radius: 14px;
|
||||
font-size: 1.75rem;
|
||||
background: linear-gradient(135deg, rgba(245, 158, 11, 0.2), rgba(245, 158, 11, 0.06));
|
||||
border: 1px solid rgba(245, 158, 11, 0.25);
|
||||
}
|
||||
|
||||
.communityModalTitle {
|
||||
margin: 0 0 0.35rem;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
color: var(--foreground);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.communityModalDesc {
|
||||
margin: 0 0 1.25rem;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
color: var(--muted-foreground);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.communityModalRows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.65rem;
|
||||
margin-bottom: 1.25rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.communityModalRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.65rem 0.85rem;
|
||||
border-radius: 10px;
|
||||
background: var(--muted);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.communityModalLabel {
|
||||
flex-shrink: 0;
|
||||
width: 2.75rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.communityModalValueWrap {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.communityModalCopyable {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
justify-content: flex-end !important;
|
||||
}
|
||||
|
||||
.communityModalMono {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 0.85rem;
|
||||
word-break: break-all;
|
||||
text-align: right;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.communityModalOpenLink {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 6px;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.communityModalOpenLink:hover {
|
||||
background: rgba(245, 158, 11, 0.12);
|
||||
}
|
||||
|
||||
.communityModalDone {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
color: var(--accent-foreground);
|
||||
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
||||
box-shadow: 0 4px 14px rgba(245, 158, 11, 0.35);
|
||||
transition: transform 0.15s, box-shadow 0.15s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.communityModalDone:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 18px rgba(245, 158, 11, 0.45);
|
||||
}
|
||||
|
||||
/* ========== 首页 · 常见问题 ========== */
|
||||
.faqSection {
|
||||
padding: 2rem 0 2.5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.faqSection {
|
||||
padding: 2.5rem 0 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.faqInner {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.faqEyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.faqEyebrowDot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.2);
|
||||
}
|
||||
|
||||
.faqHeadline {
|
||||
font-size: clamp(1.35rem, 3.5vw, 1.75rem);
|
||||
font-weight: 700;
|
||||
color: var(--foreground);
|
||||
margin: 0 0 0.35rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.faqSub {
|
||||
font-size: 0.9rem;
|
||||
color: var(--muted-foreground);
|
||||
margin: 0 0 1.5rem;
|
||||
line-height: 1.55;
|
||||
max-width: 36rem;
|
||||
}
|
||||
|
||||
.faqList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.faqItem {
|
||||
border-radius: 14px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--card);
|
||||
overflow: hidden;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.faqItem:hover {
|
||||
border-color: rgba(245, 158, 11, 0.35);
|
||||
}
|
||||
|
||||
.faqItemOpen {
|
||||
border-color: rgba(245, 158, 11, 0.45);
|
||||
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.06), 0 0 0 1px rgba(245, 158, 11, 0.08);
|
||||
}
|
||||
|
||||
.dark .faqItemOpen {
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(245, 158, 11, 0.12);
|
||||
}
|
||||
|
||||
.faqTrigger {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
gap: 0.85rem;
|
||||
padding: 1rem 1.1rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
font: inherit;
|
||||
color: var(--foreground);
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.faqTrigger:hover {
|
||||
background: var(--muted);
|
||||
}
|
||||
|
||||
.faqTrigger:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.faqIndex {
|
||||
flex-shrink: 0;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
background: rgba(245, 158, 11, 0.12);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.faqQuestionText {
|
||||
flex: 1;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
|
||||
.faqChevron {
|
||||
flex-shrink: 0;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
color: var(--muted-foreground);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), color 0.2s, background 0.2s;
|
||||
}
|
||||
|
||||
.faqItemOpen .faqChevron {
|
||||
transform: rotate(180deg);
|
||||
color: var(--accent);
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
}
|
||||
|
||||
.faqPanel {
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
transition: grid-template-rows 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.faqPanelOpen {
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
|
||||
.faqPanelInner {
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.faqAnswer {
|
||||
padding: 0 1.1rem 1.1rem 3.65rem;
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.65;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.faqAnswer {
|
||||
padding-left: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 首页 · 立即加入 CTA ========== */
|
||||
.ctaSection {
|
||||
padding: 0 0 2.75rem;
|
||||
}
|
||||
|
||||
.ctaInner {
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ctaGlow {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
inset: -20% -10% auto;
|
||||
height: 70%;
|
||||
background: radial-gradient(
|
||||
ellipse 70% 80% at 50% 0%,
|
||||
rgba(245, 158, 11, 0.22),
|
||||
transparent 65%
|
||||
);
|
||||
opacity: 0.85;
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
.ctaCard {
|
||||
position: relative;
|
||||
border-radius: 20px;
|
||||
padding: 1.75rem 1.5rem 1.65rem;
|
||||
text-align: center;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(16px) saturate(1.25);
|
||||
border: 1px solid rgba(245, 158, 11, 0.24);
|
||||
box-shadow:
|
||||
0 20px 50px rgba(0, 0, 0, 0.08),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.45) inset;
|
||||
}
|
||||
|
||||
.dark .ctaCard {
|
||||
background: rgba(20, 20, 26, 0.78);
|
||||
box-shadow:
|
||||
0 24px 56px rgba(0, 0, 0, 0.45),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.06) inset;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.ctaCard {
|
||||
padding: 2rem 2.25rem 1.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 与主标题同一行的限时标签 */
|
||||
.ctaPromoLine {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
margin: 0 0 0.85rem;
|
||||
font-size: clamp(0.8125rem, 3.4vw, 1.125rem);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.ctaBadgeInline {
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.28rem 0.65rem;
|
||||
font-size: 0.7em;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
background: rgba(245, 158, 11, 0.12);
|
||||
border: 1px solid rgba(245, 158, 11, 0.28);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.ctaPromoDivider {
|
||||
flex-shrink: 0;
|
||||
color: var(--muted-foreground);
|
||||
opacity: 0.55;
|
||||
font-weight: 500;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.ctaPromoMain {
|
||||
display: inline-flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: baseline;
|
||||
gap: 0.25em;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ctaPromoSuffix {
|
||||
flex-shrink: 0;
|
||||
color: var(--foreground);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.ctaBadgePulse {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
animation: ctaPulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes ctaPulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.65; transform: scale(0.92); }
|
||||
}
|
||||
|
||||
.ctaHighlight {
|
||||
background: linear-gradient(120deg, var(--accent) 0%, #fbbf24 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.ctaChips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
justify-content: center;
|
||||
margin-bottom: 0.85rem;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.ctaChip {
|
||||
font-size: clamp(0.5625rem, 2.1vw, 0.6875rem);
|
||||
font-weight: 500;
|
||||
color: var(--muted-foreground);
|
||||
padding: 0.2rem 0.45rem;
|
||||
border-radius: 9999px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--muted);
|
||||
opacity: 0.68;
|
||||
}
|
||||
|
||||
/* 与 SHOW_CTA_AUDIENCE_BOUNDARY 配合:隐藏时不删 DOM,仅不占布局 */
|
||||
.ctaBoundaryHidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ctaBoundary {
|
||||
position: relative;
|
||||
margin: 0 auto 1.4rem;
|
||||
max-width: 26rem;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
border: 1px solid rgba(245, 158, 11, 0.22);
|
||||
background:
|
||||
linear-gradient(125deg, rgba(245, 158, 11, 0.1) 0%, transparent 45%),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.75) 0%, rgba(248, 248, 250, 0.94) 100%);
|
||||
box-shadow:
|
||||
0 12px 40px rgba(0, 0, 0, 0.06),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.5) inset;
|
||||
}
|
||||
|
||||
.dark .ctaBoundary {
|
||||
background:
|
||||
linear-gradient(125deg, rgba(245, 158, 11, 0.14) 0%, transparent 48%),
|
||||
linear-gradient(180deg, rgba(32, 32, 40, 0.92) 0%, rgba(16, 16, 20, 0.96) 100%);
|
||||
border-color: rgba(245, 158, 11, 0.28);
|
||||
box-shadow:
|
||||
0 16px 48px rgba(0, 0, 0, 0.38),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05) inset;
|
||||
}
|
||||
|
||||
.ctaBoundarySheen {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -50%;
|
||||
width: 42%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
100deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.28) 48%,
|
||||
transparent 72%
|
||||
);
|
||||
animation: ctaBoundaryShine 8s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dark .ctaBoundarySheen {
|
||||
background: linear-gradient(
|
||||
100deg,
|
||||
transparent 0%,
|
||||
rgba(251, 191, 36, 0.1) 48%,
|
||||
transparent 72%
|
||||
);
|
||||
}
|
||||
|
||||
@keyframes ctaBoundaryShine {
|
||||
0%, 100% {
|
||||
transform: translateX(-20%);
|
||||
opacity: 0;
|
||||
}
|
||||
40% {
|
||||
opacity: 0.85;
|
||||
}
|
||||
55% {
|
||||
transform: translateX(380%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ctaBoundaryInner {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 0.85rem 1rem 0.95rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
.ctaBoundaryPos {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
font-size: clamp(0.75rem, 2.4vw, 0.875rem);
|
||||
line-height: 1.52;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.ctaBoundaryNeg {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
font-size: clamp(0.7rem, 2.15vw, 0.8125rem);
|
||||
line-height: 1.52;
|
||||
color: var(--muted-foreground);
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px dashed rgba(245, 158, 11, 0.28);
|
||||
}
|
||||
|
||||
.ctaBoundaryMark {
|
||||
flex-shrink: 0;
|
||||
width: 1.4rem;
|
||||
height: 1.4rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.62rem;
|
||||
line-height: 1;
|
||||
color: var(--accent);
|
||||
background: rgba(245, 158, 11, 0.16);
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(245, 158, 11, 0.28);
|
||||
}
|
||||
|
||||
.ctaBoundaryMarkNeg {
|
||||
flex-shrink: 0;
|
||||
width: 1.4rem;
|
||||
text-align: center;
|
||||
font-weight: 800;
|
||||
font-size: 1rem;
|
||||
line-height: 1.4rem;
|
||||
color: rgba(220, 80, 80, 0.88);
|
||||
}
|
||||
|
||||
.dark .ctaBoundaryMarkNeg {
|
||||
color: rgba(252, 165, 165, 0.88);
|
||||
}
|
||||
|
||||
.ctaBoundaryStrong {
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.ctaBoundaryStrongNeg {
|
||||
font-weight: 600;
|
||||
color: rgba(220, 80, 80, 0.95);
|
||||
}
|
||||
|
||||
.dark .ctaBoundaryStrongNeg {
|
||||
color: rgba(254, 202, 202, 0.92);
|
||||
}
|
||||
|
||||
.ctaActions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
@media (min-width: 480px) {
|
||||
.ctaActions {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.ctaBtnPrimary {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
max-width: 280px;
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 6px 20px rgba(245, 158, 11, 0.35);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (min-width: 480px) {
|
||||
.ctaBtnPrimary {
|
||||
margin: 0;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.ctaBtnPrimary::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
120deg,
|
||||
transparent 40%,
|
||||
rgba(255, 255, 255, 0.18) 50%,
|
||||
transparent 60%
|
||||
);
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.6s ease;
|
||||
}
|
||||
|
||||
.ctaBtnPrimary:hover::after {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.ctaBtnSecondary {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
max-width: 280px;
|
||||
margin: 0 auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem 1.75rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
border-radius: 12px;
|
||||
color: var(--foreground);
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: transform 0.15s, border-color 0.15s, color 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
@media (min-width: 480px) {
|
||||
.ctaBtnSecondary {
|
||||
margin: 0;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.ctaBtnSecondary:hover {
|
||||
transform: translateY(-1px);
|
||||
background: var(--muted);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user