From 1f4473cb049f83d264e97e96b6076f01e0be60cf Mon Sep 17 00:00:00 2001 From: eric Date: Sun, 8 Mar 2026 06:43:48 -0500 Subject: [PATCH] =?UTF-8?q?'s'=E8=B0=83=E8=AF=95=E6=94=AF=E4=BB=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/[locale]/course/page.tsx | 18 +- app/[locale]/ebook/EbookClient.tsx | 2 +- app/[locale]/join/page.tsx | 46 ++-- app/[locale]/join/paid/page.tsx | 31 +++ app/[locale]/layout.tsx | 16 +- app/[locale]/page.tsx | 7 +- app/api/join/route.ts | 13 +- app/api/pay/route.ts | 77 ++++-- app/api/pay/status/route.ts | 7 +- app/api/upload-media/route.ts | 35 +-- app/components/AuthModal.tsx | 52 +---- app/components/Header.tsx | 18 +- app/components/Tools.tsx | 16 +- app/layout.tsx | 15 +- app/lib/minio/client.ts | 67 ++++++ app/lib/minio/config.ts | 2 + app/lib/minio/index.ts | 13 ++ app/lib/payment/client.ts | 57 +++++ app/lib/payment/config.ts | 2 + app/lib/payment/index.ts | 4 + app/lib/payment/types.ts | 21 ++ app/lib/payment/usePayStatusPoll.ts | 76 ++++++ app/lib/pocketbase/auth.ts | 94 ++++++++ app/lib/pocketbase/config.ts | 2 + app/lib/pocketbase/constants.ts | 5 + app/lib/pocketbase/index.ts | 11 + app/lib/theme-data.ts | 86 +++++++ config/index.ts | 16 ++ config/services.config.ts | 137 +++++++++++ config/site.config.ts | 165 +++++++++++++ content/themes/digital-nomad/data/tools.json | 219 ++++++++++++++++++ content/themes/digital-nomad/messages/en.json | 155 +++++++++++++ content/themes/digital-nomad/messages/zh.json | 155 +++++++++++++ docs/SERVICES.md | 178 ++++++++++++++ docs/TEMPLATE.md | 154 ++++++++++++ 35 files changed, 1794 insertions(+), 178 deletions(-) create mode 100644 app/[locale]/join/paid/page.tsx create mode 100644 app/lib/minio/client.ts create mode 100644 app/lib/minio/config.ts create mode 100644 app/lib/minio/index.ts create mode 100644 app/lib/payment/client.ts create mode 100644 app/lib/payment/config.ts create mode 100644 app/lib/payment/index.ts create mode 100644 app/lib/payment/types.ts create mode 100644 app/lib/payment/usePayStatusPoll.ts create mode 100644 app/lib/pocketbase/auth.ts create mode 100644 app/lib/pocketbase/config.ts create mode 100644 app/lib/pocketbase/constants.ts create mode 100644 app/lib/pocketbase/index.ts create mode 100644 app/lib/theme-data.ts create mode 100644 config/index.ts create mode 100644 config/services.config.ts create mode 100644 config/site.config.ts create mode 100644 content/themes/digital-nomad/data/tools.json create mode 100644 content/themes/digital-nomad/messages/en.json create mode 100644 content/themes/digital-nomad/messages/zh.json create mode 100644 docs/SERVICES.md create mode 100644 docs/TEMPLATE.md diff --git a/app/[locale]/course/page.tsx b/app/[locale]/course/page.tsx index 16551d5..9b551b2 100644 --- a/app/[locale]/course/page.tsx +++ b/app/[locale]/course/page.tsx @@ -4,18 +4,7 @@ import { useState, useEffect } from "react"; import { VideoCard } from "./VideoCard"; import { useVideoProgress } from "./useVideoProgress"; import AuthModal from "../../components/AuthModal"; - -function getStoredUser(): string | null { - if (typeof window === "undefined") return null; - try { - const raw = localStorage.getItem("pb_user"); - if (!raw) return null; - const user = JSON.parse(raw); - return user?.email ?? null; - } catch { - return null; - } -} +import { getStoredUserEmail, pbLogout } from "@/app/lib/pocketbase"; /* ─── data ─── */ @@ -166,12 +155,11 @@ export default function CoursePage() { const { markCompleted, isCompleted, isPartCompleted } = useVideoProgress(); useEffect(() => { - setUserEmail(getStoredUser()); + setUserEmail(getStoredUserEmail()); }, []); const handleLogout = () => { - localStorage.removeItem("pb_token"); - localStorage.removeItem("pb_user"); + pbLogout(); setUserEmail(null); }; diff --git a/app/[locale]/ebook/EbookClient.tsx b/app/[locale]/ebook/EbookClient.tsx index d6119b7..6c22e16 100644 --- a/app/[locale]/ebook/EbookClient.tsx +++ b/app/[locale]/ebook/EbookClient.tsx @@ -2,7 +2,7 @@ import { useEffect, useState, useRef, useCallback } from "react"; import { Link } from "@/i18n/navigation"; -import type { EbookData } from "../lib/ebook"; +import type { EbookData } from "@/app/lib/ebook"; import "github-markdown-css/github-markdown.css"; import styles from "./ebook.module.css"; diff --git a/app/[locale]/join/page.tsx b/app/[locale]/join/page.tsx index b475ee6..d9fe8a6 100644 --- a/app/[locale]/join/page.tsx +++ b/app/[locale]/join/page.tsx @@ -2,7 +2,15 @@ import { useState, useRef, useEffect, type FormEvent, type ChangeEvent } from "react"; import { Link } from "@/i18n/navigation"; -import { getPayEnv, getRecommendedChannel, mustUseWxPay, type PayChannel, type PayEnv } from "../../lib/payEnv"; +import { + getPayEnv, + getRecommendedChannel, + mustUseWxPay, + redirectToPay, + getDeviceFromEnv, +} from "@/app/lib/payment"; +import { usePayStatusPoll } from "@/app/lib/payment/usePayStatusPoll"; +import type { PayChannel, PayEnv } from "@/app/lib/payment"; const genderOptions = ["男", "女"]; const singleOptions = ["单身", "恋爱中", "已婚"]; @@ -55,6 +63,9 @@ export default function JoinPage() { } }, []); + // 微信 H5 支付完成后可能不跳转,后台静默轮询,检测到已支付则显示成功页 + usePayStatusPoll(() => setSubmitted(true)); + useEffect(() => { if (typeof window === "undefined") return; const env = getPayEnv(); @@ -86,33 +97,18 @@ export default function JoinPage() { } setSubmitting(false); setPayRedirecting(true); - const returnUrl = `${window.location.origin}${window.location.pathname}?paid=1`; - const payForm = document.createElement("form"); - payForm.method = "POST"; - payForm.action = "/api/pay"; - payForm.target = "_self"; - payForm.style.display = "none"; + // Z-Pay return_url 不支持带参数,使用 /join/paid 专用路径 + const returnUrl = `${window.location.origin}${window.location.pathname}/paid`; const env = getPayEnv(); const channel = mustUseWxPay(env) ? "wxpay" : payChannel; - const device = env === "pc" ? "pc" : "h5"; // PC 跳转 payurl,H5/微信 用 payurl/payurl2 - const fields: [string, string][] = [ - ["user_id", userId], - ["total_fee", "80"], - ["type", "meetup"], - ["channel", channel], - ["device", device], - ["return_url", returnUrl], - ["html", "1"], - ]; - fields.forEach(([k, v]) => { - const input = document.createElement("input"); - input.type = "hidden"; - input.name = k; - input.value = v; - payForm.appendChild(input); + const device = getDeviceFromEnv(env); + redirectToPay({ + user_id: userId, + return_url: returnUrl, + channel, + device, + useJoinConfig: true, }); - document.body.appendChild(payForm); - payForm.submit(); return; } catch (err) { setError(err instanceof Error ? err.message : "网络错误,请重试"); diff --git a/app/[locale]/join/paid/page.tsx b/app/[locale]/join/paid/page.tsx new file mode 100644 index 0000000..4cab2ea --- /dev/null +++ b/app/[locale]/join/paid/page.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { Link } from "@/i18n/navigation"; + +/** + * 支付完成回跳页 + * Z-Pay 的 return_url 不支持带查询参数,故使用 /join/paid 作为专用成功页 + */ +export default function JoinPaidPage() { + return ( +
+
+
+ ✅ +
+

申请已提交

+

+ 我们会尽快审核你的信息 +
+ 审核通过后将通过微信邀请你入群 +

+ + ← 返回首页 + +
+
+ ); +} diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx index 42a44b8..6e28e81 100644 --- a/app/[locale]/layout.tsx +++ b/app/[locale]/layout.tsx @@ -2,14 +2,18 @@ import type { Metadata } from "next"; import { notFound } from "next/navigation"; import { LocaleProvider } from "../context/LocaleContext"; import type { Locale } from "../context/LocaleContext"; +import { getThemeConfig } from "@/config"; +import { getThemeMessages } from "@/app/lib/theme-data"; const LOCALES: Locale[] = ["zh", "en"]; -export const metadata: Metadata = { - title: "数字游民指南 | Digital Nomad Guide", - description: - "从零开始,7天开启你的数字游民生活。远程工作、地点自由、技能变现 —— 一站式数字游民资源平台。", -}; +export const metadata: Metadata = (() => { + const theme = getThemeConfig(); + return { + title: theme.meta.title, + description: theme.meta.description, + }; +})(); export function generateStaticParams() { return LOCALES.map((locale) => ({ locale })); @@ -27,7 +31,7 @@ export default async function LocaleLayout({ notFound(); } - const messages = (await import(`../../messages/${locale}.json`)).default; + const messages = await getThemeMessages(locale as Locale); return ( diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index 0cbb240..1610e14 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -5,8 +5,11 @@ import Roadmap from "../components/Roadmap"; import Tools from "../components/Tools"; import Community from "../components/Community"; import Footer from "../components/Footer"; +import { getThemeToolsData } from "../lib/theme-data"; + +export default async function HomePage() { + const toolsData = await getThemeToolsData(); -export default function HomePage() { return ( <>
@@ -14,7 +17,7 @@ export default function HomePage() { - +