Files
gitlab-instance-0a899031_no…/app/page.tsx
2026-03-10 11:48:50 -05:00

512 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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";
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";
function redirectToPayZpay(params: {
user_id: string;
return_url: string;
total_fee: number;
type: string;
channel: "alipay" | "wxpay";
device: "pc" | "h5" | "wechat";
}) {
const form = document.createElement("form");
form.method = "POST";
form.action = "/api/pay";
form.target = "_self";
form.style.display = "none";
const fields: [string, string][] = [
["user_id", params.user_id],
["return_url", params.return_url],
["total_fee", String(params.total_fee)],
["type", params.type],
["channel", params.channel],
["device", params.device],
["provider", "zpay"],
["html", "1"],
];
fields.forEach(([k, v]) => {
const input = document.createElement("input");
input.type = "hidden";
input.name = k;
input.value = v;
form.appendChild(input);
});
document.body.appendChild(form);
form.submit();
}
async function redirectToPayZpayH5(params: {
user_id: string;
return_url: string;
total_fee: number;
type: string;
channel: "alipay" | "wxpay";
}): Promise<string | null> {
const res = await fetch("/api/pay", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
...params,
device: "h5",
provider: "zpay",
html: "0",
}),
});
const data = await res.json().catch(() => ({}));
if (!data?.ok || !data?.params || !data?.payUrl) {
alert(data?.error || "支付发起失败");
return null;
}
const orderId = data?.order_id?.trim() || null;
if (data?.redirect_url) {
window.open(data.redirect_url, "_blank");
return orderId;
}
const form = document.createElement("form");
form.method = "POST";
form.action = data.payUrl;
form.target = "_blank";
form.style.display = "none";
for (const [k, v] of Object.entries(data.params as Record<string, string>)) {
if (v == null) continue;
const input = document.createElement("input");
input.type = "hidden";
input.name = k;
input.value = String(v);
form.appendChild(input);
}
document.body.appendChild(form);
form.submit();
document.body.removeChild(form);
return orderId;
}
async function payWechatXorpay(params: {
user_id: string;
return_url: string;
total_fee: number;
type: string;
channel: "alipay" | "wxpay";
}) {
const res = await fetch("/api/pay", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
...params,
total_fee: params.total_fee,
device: "wechat",
provider: "xorpay",
html: "0",
}),
});
const data = await res.json().catch(() => ({}));
if (!data?.ok || !data?.params || !data?.payUrl) {
alert(data?.error || "支付发起失败");
return;
}
const form = document.createElement("form");
form.method = "POST";
form.action = data.payUrl;
form.target = "_self";
form.style.display = "none";
for (const [k, v] of Object.entries(data.params as Record<string, string>)) {
if (v == null) continue;
const input = document.createElement("input");
input.type = "hidden";
input.name = k;
input.value = String(v);
form.appendChild(input);
}
document.body.appendChild(form);
form.submit();
}
function getStorage(key: string): string | null {
if (typeof window === "undefined") return null;
return localStorage.getItem(key);
}
function setStorage(key: string, value: string): void {
if (typeof window === "undefined") return;
localStorage.setItem(key, value);
}
function removeStorage(key: string): void {
if (typeof window === "undefined") return;
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);
const [loading, setLoading] = useState(true);
const [payPendingOrderId, setPayPendingOrderId] = useState<string | null>(null);
const savePaidType = useCallback((type: string | null) => {
if (type) setStorage("paidType", type);
else removeStorage("paidType");
setPaidType(type);
}, []);
const saveUserName = useCallback((name: string) => {
setStorage("userName", name);
setUserName(name);
}, []);
const getOrCreateUserId = useCallback(() => {
let userId = getStorage("userId");
if (userId?.startsWith("{") && userId.includes("data")) {
try {
const parsed = JSON.parse(userId) as { data?: string };
if (parsed?.data) {
userId = parsed.data;
setStorage("userId", userId);
}
} catch {
userId = null;
}
}
if (!userId) {
userId = `user${Date.now()}`;
setStorage("userId", userId);
}
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",
});
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 {
savePaidType(null);
return null;
}
} catch (err) {
console.error("查询 PocketBase 失败:", err);
return getStorage("paidType");
}
}, [savePaidType, saveUserName]);
const clearAllStorage = useCallback(() => {
try {
if (typeof window === "undefined") return;
const keys = ["userId", "paidType", "userName"];
keys.forEach((k) => {
try {
localStorage.removeItem(k);
} catch {
/* 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);
const path = window.location.pathname || "/";
window.location.replace(path);
} catch (error) {
console.error("清空存储失败:", error);
}
}, []);
const handlePay = useCallback(async () => {
const userId = getOrCreateUserId();
const returnUrl = `${window.location.origin}/paid`;
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 };
if (device === "wechat" && WECHAT_PAY_PROVIDER === "xorpay") {
payWechatXorpay(base);
} else if (device === "h5") {
const orderId = await redirectToPayZpayH5(base);
if (orderId) setPayPendingOrderId(orderId);
} else {
redirectToPayZpay({ ...base, device });
}
}, [getOrCreateUserId]);
const handleCopyUsername = useCallback(() => {
const name = userName || "";
if (typeof navigator !== "undefined" && navigator.clipboard) {
navigator.clipboard.writeText(name);
alert("账号已复制");
}
}, [userName]);
const env = getPayEnv();
const device = getDeviceFromEnv(env);
const isH5 = device === "h5";
usePayStatusPoll(
(orderId) => {
let uid = getStorage("userId");
if (!uid && orderId?.includes("_order_")) {
const prefix = orderId.split("_order_")[0];
const parts = prefix.split("_");
if (parts.length >= 2) uid = parts.slice(0, -1).join("_");
}
if (uid) saveUserName(uid);
savePaidType("vip");
if (isH5) setPayPendingOrderId(null);
window.location.reload();
},
() => {
if (isH5) setPayPendingOrderId(null);
},
isH5 ? payPendingOrderId : undefined
);
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get("type")?.trim() === "clean") {
clearAllStorage();
return;
}
if (urlParams.get("paid") === "1") {
savePaidType("vip");
saveUserName(getStorage("userId") || "");
}
const init = async () => {
getOrCreateUserId();
const localType = getStorage("paidType");
const localUserName = getStorage("userName");
if (localType && localUserName) {
setPaidType(localType);
setUserName(localUserName);
setLoading(false);
return;
}
const userId = getStorage("userId");
if (userId) {
await checkPaymentFromPB(userId);
}
setLoading(false);
};
init();
}, [clearAllStorage, getOrCreateUserId, checkPaymentFromPB, savePaidType, saveUserName]);
useEffect(() => {
const syncFromStorage = () => {
const localType = getStorage("paidType");
const localUserName = getStorage("userName");
if (localType && localUserName && !paidType) {
setPaidType(localType);
setUserName(localUserName);
}
};
const onVisible = () => syncFromStorage();
const onPageShow = (e: PageTransitionEvent) => {
if (e.persisted) syncFromStorage();
};
if (typeof document !== "undefined" && document.addEventListener) {
document.addEventListener("visibilitychange", onVisible);
window.addEventListener("pageshow", onPageShow);
return () => {
document.removeEventListener("visibilitychange", onVisible);
window.removeEventListener("pageshow", onPageShow);
};
}
}, [paidType]);
if (loading) {
return (
<div className={styles.loading}>
<div className={styles.loadingSpinner} />
<p>...</p>
</div>
);
}
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>
);
}