Files
2026-03-10 04:41:20 -05:00

311 lines
9.0 KiB
TypeScript
Raw Permalink 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, useRef, useState } from "react";
import Image from "next/image";
import { marked } from "marked";
import PocketBase from "pocketbase";
import { indexMd } from "./index-content";
import styles from "./index.module.css";
import {
getPayEnv,
getRecommendedChannel,
mustUseWxPay,
getDeviceFromEnv,
} from "@/app/lib/payEnv";
import { usePayStatusPoll } from "@/app/lib/usePayStatusPoll";
const PB_URL =
process.env.NEXT_PUBLIC_POCKETBASE_URL || "https://pocketbase.hackrobot.cn";
function redirectToPay(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],
["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();
}
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);
}
export default function Home() {
const [paidType, setPaidType] = useState<string | null>(null);
const [userName, setUserName] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const contentRef = useRef<HTMLDivElement>(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) {
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" && window.localStorage) {
window.localStorage.clear();
}
removeStorage("userId");
removeStorage("paidType");
removeStorage("userName");
setPaidType(null);
setUserName(null);
setLoading(false);
alert("本地存储已清空");
// 移除 URL 中的 ?type=clean避免刷新时重复触发
if (typeof window !== "undefined" && window.history.replaceState) {
window.history.replaceState({}, "", window.location.pathname);
}
} catch (error) {
console.error("清空存储失败:", error);
alert("清空存储失败");
}
}, []);
const handlePay = useCallback(() => {
const userId = getOrCreateUserId();
const returnUrl = `${window.location.origin}/paid`;
const env = getPayEnv();
const channel = mustUseWxPay(env) ? "wxpay" : getRecommendedChannel(env);
const device = getDeviceFromEnv(env);
redirectToPay({
user_id: userId,
return_url: returnUrl,
total_fee: 18800,
type: "vip",
channel,
device,
});
}, [getOrCreateUserId]);
const handleCopyUsername = useCallback(() => {
const name = userName || "";
if (typeof navigator !== "undefined" && navigator.clipboard) {
navigator.clipboard.writeText(name);
alert("账号已复制");
}
}, [userName]);
// 微信 H5 支付完成后可能不跳转,轮询检测到已支付则刷新
usePayStatusPoll(() => {
savePaidType("vip");
saveUserName(getStorage("userId") || "");
});
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get("type") === "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]);
marked.setOptions({ breaks: true, gfm: true });
return (
<div className={styles.index}>
<div className={`${styles.indexBook} animate__animated animate__pulse`}>
<div className={styles.indexBookLeft}>
<div className={styles.leftTitle}></div>
<div className={styles.card}>
<div className={styles.cardItem}>
<div className={styles.cardItemLeft}>
<Image
className={styles.cardItemLeftImg}
src="/images/group.svg"
alt="group"
width={40}
height={40}
/>
</div>
<div className={styles.cardItemRight}>
<div className={styles.cardItemRightUp}></div>
<div className={styles.cardItemRightDown}></div>
</div>
</div>
</div>
<div
ref={contentRef}
className={`markdown-body ${styles.markVip}`}
dangerouslySetInnerHTML={{ __html: marked(indexMd) as string }}
/>
<div className={styles.leftBtn}>
{paidType === "vip" ? (
<div className={styles.vipcode}>
<div
onClick={() => {
window.location.href = "https://qun.hackrobot.cn";
}}
style={{ cursor: "pointer" }}
>
👉星球地址: qun.hackrobot.cn
</div>
<div>:{getStorage("userName")}</div>
<button onClick={handleCopyUsername}></button>
<div>密码:123456</div>
<div>,&quot;&quot;,</div>
</div>
) : (
<button
onClick={() => {
if (paidType) return;
handlePay();
}}
className={styles.startRead}
type="button"
>
🔒 vip会员
</button>
)}
</div>
</div>
<div className={styles.indexBookImg}>
<Image
className={styles.bookImg}
src="/images/touxiang.png"
alt="avatar"
width={200}
height={200}
/>
</div>
</div>
<div className={styles.svgItems}>
<div className={styles.svgItem}>
<div className={styles.bookImgItem}>
<Image
src="/images/laptop.svg"
alt="laptop"
width={80}
height={80}
/>
<div className={styles.bookImgText}></div>
</div>
<div className={styles.bookImgItem}>
<Image
src="/images/travel.svg"
alt="travel"
width={80}
height={80}
/>
<div className={styles.bookImgText}>RPA自动化</div>
</div>
<div className={styles.bookImgItem}>
<Image
src="/images/code.svg"
alt="code"
width={80}
height={80}
/>
<div className={styles.bookImgText}></div>
</div>
</div>
</div>
</div>
);
}