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

371 lines
11 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, 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, getDeviceFromEnv } from "@/app/lib/payEnv";
import { getPaymentConfig } from "@/config/services.config";
import { usePayStatusPoll } from "@/app/lib/usePayStatusPoll";
const PB_URL =
process.env.NEXT_PUBLIC_POCKETBASE_URL || "https://pocketbase.hackrobot.cn";
/** PC/H5form POST 到 /api/pay有中转页默认 zpay */
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();
}
/** 微信内fetch 获取 xorpay 参数,直接提交表单到收银台,无中转页 */
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);
}
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?.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(() => {
const userId = getOrCreateUserId();
const returnUrl = `${window.location.origin}/paid`;
const env = getPayEnv();
const device = getDeviceFromEnv(env);
const totalFee = getPaymentConfig().defaultAmount ?? 880;
const base = { user_id: userId, return_url: returnUrl, total_fee: totalFee, type: "vip", channel: "wxpay" as const };
if (device === "wechat") {
payWechatXorpay(base);
} else {
redirectToPayZpay({ ...base, 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")?.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]);
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>
);
}