311 lines
9.0 KiB
TypeScript
311 lines
9.0 KiB
TypeScript
"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>打开网页后,请点击"左上角头像",打开侧面栏</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>
|
||
);
|
||
}
|