This commit is contained in:
eric
2026-03-11 23:47:45 -05:00
parent 296d703307
commit 0c2ccadb6b
35 changed files with 2399 additions and 195 deletions

View File

@@ -1,21 +1,18 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import Link from "next/link";
import PocketBase from "pocketbase";
import { getPayEnv, getDeviceFromEnv } from "@/app/lib/payEnv";
import { getPaymentConfig } from "@/config/services.config";
import { formatRestoreDateShort } from "@/config/promo.config";
import { usePayStatusPoll } from "@/app/lib/usePayStatusPoll";
import { ThemeToggle } from "@/app/components/ThemeToggle";
import styles from "./home.module.css";
import { VipLayout } from "@/app/vip/components/VipLayout";
import { LandingPage } from "@/app/vip/components/LandingPage";
import { DashboardPage } from "@/app/vip/components/DashboardPage";
import styles from "@/app/vip/components/vip.module.css";
const PB_URL =
process.env.NEXT_PUBLIC_POCKETBASE_URL || "https://pocketbase.hackrobot.cn";
/** 临时:预览支付成功后的界面,本地调试用,上线前改回 false */
const FORCE_VIP_PREVIEW = false;
/** 微信内支付渠道zpay | xorpay */
const WECHAT_PAY_PROVIDER: "zpay" | "xorpay" = "xorpay";
@@ -154,33 +151,6 @@ function removeStorage(key: string): void {
localStorage.removeItem(key);
}
const FEATURES = [
{
icon: "🚀",
title: "业务方向",
desc: "无人直播、RPA 自动化、Web 开发,找到你的变现路径",
},
{
icon: "⚙️",
title: "技术杠杆",
desc: "云手机、PVE 虚拟化、服务器集群,低成本放大收益",
},
{
icon: "🌐",
title: "海外平台",
desc: "YouTube、TikTok、SaaS 运营实战,出海掘金",
},
];
const UNLOCK_ITEMS = [
{ icon: "📖", title: "电子书", desc: "地理套利·自动化", href: "/ebook" },
{ icon: "🎬", title: "视频教程", desc: "云手机·无人直播", href: "/cloudphone" },
{ icon: "👥", title: "私密社群", desc: "抱团出海", href: null },
{ icon: "🛒", title: "商店", desc: "设备自助", href: null },
{ icon: "💬", title: "即时问答", desc: "技术咨询", href: "https://chatwoot.hackrobot.cn/livechat?user_id=hackrobot" },
{ icon: "🎪", title: "线下沙龙", desc: "面交·对接", href: null },
];
export default function Home() {
const [paidType, setPaidType] = useState<string | null>(null);
const [userName, setUserName] = useState<string | null>(null);
@@ -218,29 +188,32 @@ export default function Home() {
return userId;
}, []);
const checkPaymentFromPB = useCallback(async (userId: string) => {
try {
const pb = new PocketBase(PB_URL);
const records = await pb.collection("payments").getList(1, 1, {
filter: `user_id = "${userId}"`,
sort: "-created",
});
const checkPaymentFromPB = useCallback(
async (userId: string): Promise<boolean> => {
try {
const pb = new PocketBase(PB_URL);
const records = await pb.collection("payments").getList(1, 1, {
filter: `user_id = "${userId}"`,
sort: "-created",
});
if (records.items.length > 0) {
const latestType = records.items[0].type as string;
const dbUserId = records.items[0].user_id as string;
savePaidType(latestType);
saveUserName(dbUserId);
return latestType;
} else {
if (records.items.length > 0) {
const latestType = records.items[0].type as string;
const dbUserId = records.items[0].user_id as string;
setStorage("userId", dbUserId);
savePaidType(latestType);
saveUserName(dbUserId);
return latestType === "vip";
}
savePaidType(null);
return null;
return false;
} catch (err) {
console.error("查询 PocketBase 失败:", err);
return false;
}
} catch (err) {
console.error("查询 PocketBase 失败:", err);
return getStorage("paidType");
}
}, [savePaidType, saveUserName]);
},
[savePaidType, saveUserName]
);
const clearAllStorage = useCallback(() => {
try {
@@ -253,19 +226,6 @@ export default function Home() {
/* ignore */
}
});
try {
for (let i = localStorage.length - 1; i >= 0; i--) {
const key = localStorage.key(i);
if (key) localStorage.removeItem(key);
}
} catch {
/* ignore */
}
try {
sessionStorage.clear();
} catch {
/* ignore */
}
setPaidType(null);
setUserName(null);
setLoading(false);
@@ -282,7 +242,13 @@ export default function Home() {
const env = getPayEnv();
const device = getDeviceFromEnv(env);
const totalFee = getPaymentConfig().defaultAmount ?? 8800;
const base = { user_id: userId, return_url: returnUrl, total_fee: totalFee, type: "vip", channel: "wxpay" as const };
const base = {
user_id: userId,
return_url: returnUrl,
total_fee: totalFee,
type: "vip",
channel: "wxpay" as const,
};
if (device === "wechat" && WECHAT_PAY_PROVIDER === "xorpay") {
payWechatXorpay(base);
} else if (device === "h5") {
@@ -293,13 +259,13 @@ export default function Home() {
}
}, [getOrCreateUserId]);
const handleCopyUsername = useCallback(() => {
const name = userName || "";
if (typeof navigator !== "undefined" && navigator.clipboard) {
navigator.clipboard.writeText(name);
alert("账号已复制");
}
}, [userName]);
const handleLogin = useCallback(() => {
document.getElementById("login")?.scrollIntoView({ behavior: "smooth" });
}, []);
const handleVerifySuccess = useCallback(() => {
window.location.reload();
}, []);
const env = getPayEnv();
const device = getDeviceFromEnv(env);
@@ -348,7 +314,10 @@ export default function Home() {
const userId = getStorage("userId");
if (userId) {
await checkPaymentFromPB(userId);
const found = await checkPaymentFromPB(userId);
if (found) {
setUserName(getStorage("userName"));
}
}
setLoading(false);
};
@@ -379,6 +348,8 @@ export default function Home() {
}
}, [paidType]);
const isLoggedIn = paidType === "vip";
if (loading) {
return (
<div className={styles.loading}>
@@ -388,124 +359,18 @@ export default function Home() {
);
}
if (FORCE_VIP_PREVIEW || paidType === "vip") {
return (
<div className={styles.root}>
<header className={styles.header}>
<Link href="https://digital.hackrobot.cn/zh" className={styles.logo} target="_blank" rel="noopener">
🌍
</Link>
<nav className={styles.nav}>
<ThemeToggle />
</nav>
</header>
<main className={styles.main}>
<section className={styles.vipSuccess}>
<div className={`${styles.vipSuccessCard} animate__animated animate__zoomIn`} style={{ animationDelay: "0.1s", animationFillMode: "both" }}>
<div className={styles.vipSuccessIcon}></div>
<h1></h1>
<p className={styles.vipSuccessDesc}> VIP </p>
<div className={styles.vipCode}>
<div className={styles.vipCodeRow}>
<span></span>
<code>qun.hackrobot.cn</code>
</div>
<div className={styles.vipCodeRow}>
<span></span>
<code>{getStorage("userName") || (FORCE_VIP_PREVIEW ? "preview" : "")}</code>
<button type="button" onClick={handleCopyUsername}></button>
</div>
<div className={styles.vipCodeRow}>
<span></span>
<code>123456</code>
</div>
<a
href="https://qun.hackrobot.cn"
target="_blank"
rel="noopener noreferrer"
className={styles.vipCta}
>
</a>
<p className={styles.vipTip}></p>
</div>
</div>
</section>
</main>
</div>
);
}
return (
<div className={styles.root}>
<header className={styles.header}>
<Link href="https://digital.hackrobot.cn/zh" className={styles.logo} target="_blank" rel="noopener">
🌍
</Link>
<nav className={styles.nav}>
<ThemeToggle />
</nav>
</header>
<main className={styles.main}>
<section className={styles.hero}>
<p className={`${styles.heroBadge} animate__animated animate__fadeInDown`} style={{ animationDelay: "0.1s", animationFillMode: "both" }}>🎒 · </p>
<h1 className={`${styles.heroTitle} animate__animated animate__fadeInDown`} style={{ animationDelay: "0.2s", animationFillMode: "both" }}>
</h1>
<p className={`${styles.heroSubtitle} animate__animated animate__fadeInDown`} style={{ animationDelay: "0.3s", animationFillMode: "both" }}>
·
</p>
</section>
<section className={`${styles.section} ${styles.sectionTightTop}`}>
<div className={styles.features}>
{FEATURES.map((f, i) => (
<div key={i} className={`${styles.featureCard} animate__animated animate__fadeInUp`} style={{ animationDelay: `${0.3 + i * 0.1}s`, animationFillMode: "both" }}>
<span className={styles.featureIcon}>{f.icon}</span>
<h3>{f.title}</h3>
<p>{f.desc}</p>
</div>
))}
</div>
</section>
<section className={styles.section}>
<h2 className="animate__animated animate__fadeInDown" style={{ animationDelay: "0.1s", animationFillMode: "both" }}>🔓 VIP</h2>
<p className={`${styles.sectionDesc} animate__animated animate__fadeInUp`} style={{ animationDelay: "0.2s", animationFillMode: "both" }}>
·
</p>
<div className={styles.unlockGrid}>
{UNLOCK_ITEMS.map((item, i) => (
<div
key={i}
className={`${styles.unlockCard} ${styles.unlockCardDisabled} animate__animated animate__fadeInUp`}
style={{ animationDelay: `${0.25 + i * 0.06}s`, animationFillMode: "both" }}
>
<span className={styles.unlockIcon}>{item.icon}</span>
<span className={styles.unlockTitle}>{item.title}</span>
<span className={styles.unlockDesc}>{item.desc}</span>
</div>
))}
</div>
</section>
<section className={`${styles.section} ${styles.sectionCta}`}>
<div className={`${styles.ctaCard} animate__animated animate__zoomIn`} style={{ animationDelay: "0.2s", animationFillMode: "both" }}>
<h2> · {formatRestoreDateShort()}</h2>
<p> · · 退</p>
<p className={styles.ctaNote}>🚫 </p>
<button
type="button"
onClick={() => { handlePay(); }}
className={`${styles.btnPrimary} ${styles.btnLarge}`}
>
🔒 VIP
</button>
</div>
</section>
</main>
</div>
<VipLayout isLoggedIn={isLoggedIn} userName={userName}>
{isLoggedIn ? (
<DashboardPage userName={userName} />
) : (
<LandingPage
onJoin={handlePay}
onLogin={handleLogin}
onVerify={checkPaymentFromPB}
onVerifySuccess={handleVerifySuccess}
/>
)}
</VipLayout>
);
}