diff --git a/app/data/vip.mock.ts b/app/data/vip.mock.ts new file mode 100644 index 0000000..9a4dcc1 --- /dev/null +++ b/app/data/vip.mock.ts @@ -0,0 +1,112 @@ +/** + * VIP 首页 Mock 数据 - 集中管理 + * 后续可替换为 api.hackrobot.cn 真实接口 + */ + +export const ASSET_STATS = { + topics: 4, + ebooks: 2, + videos: 12, + templates: 8, + updateFrequency: "每周", +} as const; + +export const TOPICS = [ + { + id: "ebook", + icon: "📖", + title: "电子书", + desc: "地理套利·自动化", + href: "/ebook", + }, + { + id: "cloudphone", + icon: "🎬", + title: "视频教程", + desc: "云手机·无人直播", + href: "/cloudphone", + }, + { + id: "community", + icon: "👥", + title: "私密社群", + desc: "抱团出海", + href: "https://qun.hackrobot.cn", + }, + { + id: "tools", + icon: "🛒", + title: "设备自助", + desc: "开箱即用", + href: null, + }, +] as const; + +/** 模块展示 - 首页展示用 */ +export const TOPICS_DISPLAY = [ + { id: "ebook", icon: "📖", title: "电子书", desc: "地理套利·自动化杠杆", href: "/ebook" }, + { id: "cloudphone", icon: "🎬", title: "视频教程", desc: "云手机·无人直播", href: "/cloudphone" }, + { id: "community", icon: "👥", title: "私密社群", desc: "抱团出海", href: "https://qun.hackrobot.cn" }, + { id: "tools", icon: "🛒", title: "设备自助", desc: "开箱即用", href: null }, +] as const; + +/** 电子书展示 - 首页展示用 */ +export const EBOOKS_DISPLAY = [ + { id: "1", title: "数字游民", desc: "地理套利与自动化杠杆", href: "/ebook" }, + { id: "2", title: "云手机", desc: "低成本工作室搭建指南", href: "/cloudphone" }, +] as const; + +/** 视频教程展示 - 参考 digital.hackrobot.cn 资源导航专题,至少 4 个 */ +export const VIDEO_COURSES_DISPLAY = [ + { id: "cloudphone", icon: "☁️", title: "云手机模块", desc: "低成本自建云手机,TikTok/YouTube 运营搭建指南", href: "/topic/cloudphone" }, + { id: "livestream", icon: "📺", title: "无人直播模块", desc: "TikTok/YouTube 无人直播,24 小时自动化", href: "/topic/livestream" }, + { id: "indie", icon: "🛠️", title: "独立开发模块", desc: "从想法到产品,独立开发者全流程指南", href: "/topic/indie" }, + { id: "aiagent", icon: "🤖", title: "AI Agent 模块", desc: "AIGC 与智能体,Vibe Coding 跨界创造", href: "/topic/aiagent" }, +] as const; + +export const COMPARE_ITEMS = [ + { feature: "电子书", single: "单买", member: "全解锁" }, + { feature: "视频教程", single: "单买", member: "全解锁" }, + { feature: "私密社群", single: "—", member: "✓" }, + { feature: "即时问答", single: "—", member: "✓" }, + { feature: "持续更新", single: "—", member: "✓" }, +] as const; + +export const COMMUNITY_PREVIEW = [ + { icon: "👥", text: "私密社群,抱团出海" }, + { icon: "💬", text: "即时问答,技术咨询" }, + { icon: "🎪", text: "线下沙龙,面交对接" }, +] as const; + +export const FAQ_ITEMS = [ + { + q: "会员包含哪些内容?", + a: "电子书、视频教程、私密社群、即时问答、设备自助等全部解锁,并享受持续更新。", + }, + { + q: "如何加入?", + a: "点击「立即加入」完成支付即可,支付成功后自动开通,无需人工审核。", + }, + { + q: "支持退款吗?", + a: "本内容为虚拟数字交付,不支持退款。购买前请确认适合您的需求。", + }, +] as const; + +/** 已登录控制台用 */ +export const RECENT_UPDATES = [ + { id: "1", title: "云手机·第3章更新", date: "3月10日", href: "/cloudphone/course/2/0" }, + { id: "2", title: "电子书·地理套利补充", date: "3月8日", href: "/ebook" }, + { id: "3", title: "模板库·RPA 脚本新增", date: "3月5日", href: null }, +] as const; + +export const COMMUNITY_HIGHLIGHTS = [ + { id: "1", text: "本周社群讨论:出海合规要点", href: "https://qun.hackrobot.cn" }, + { id: "2", text: "问答精选:云手机多开方案", href: "https://chatwoot.hackrobot.cn/livechat?user_id=hackrobot" }, +] as const; + +export const WEEKLY_ACTIONS = [ + { id: "1", text: "完成云手机第2章", href: "/cloudphone/course/1/0", priority: 1 }, + { id: "2", text: "阅读电子书地理套利篇", href: "/ebook", priority: 2 }, + { id: "3", text: "加入社群参与讨论", href: "https://qun.hackrobot.cn", priority: 3 }, +] as const; diff --git a/app/page.tsx b/app/page.tsx index fe4cfb2..2f45852 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,21 +1,18 @@ "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"; +import { VipLayout } from "@/app/vip/components/VipLayout"; +import { LandingPage } from "@/app/vip/components/LandingPage"; +import { DashboardPage } from "@/app/vip/components/DashboardPage"; +import styles from "@/app/vip/components/vip.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"; @@ -154,33 +151,6 @@ function removeStorage(key: string): void { 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(null); const [userName, setUserName] = useState(null); @@ -218,29 +188,32 @@ export default function Home() { 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", - }); + const checkPaymentFromPB = useCallback( + async (userId: string): Promise => { + 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 { + if (records.items.length > 0) { + const latestType = records.items[0].type as string; + const dbUserId = records.items[0].user_id as string; + setStorage("userId", dbUserId); + savePaidType(latestType); + saveUserName(dbUserId); + return latestType === "vip"; + } savePaidType(null); - return null; + return false; + } catch (err) { + console.error("查询 PocketBase 失败:", err); + return false; } - } catch (err) { - console.error("查询 PocketBase 失败:", err); - return getStorage("paidType"); - } - }, [savePaidType, saveUserName]); + }, + [savePaidType, saveUserName] + ); const clearAllStorage = useCallback(() => { try { @@ -253,19 +226,6 @@ export default function Home() { /* 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); @@ -282,7 +242,13 @@ export default function Home() { 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 }; + 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") { @@ -293,13 +259,13 @@ export default function Home() { } }, [getOrCreateUserId]); - const handleCopyUsername = useCallback(() => { - const name = userName || ""; - if (typeof navigator !== "undefined" && navigator.clipboard) { - navigator.clipboard.writeText(name); - alert("账号已复制"); - } - }, [userName]); + const handleLogin = useCallback(() => { + document.getElementById("login")?.scrollIntoView({ behavior: "smooth" }); + }, []); + + const handleVerifySuccess = useCallback(() => { + window.location.reload(); + }, []); const env = getPayEnv(); const device = getDeviceFromEnv(env); @@ -348,7 +314,10 @@ export default function Home() { const userId = getStorage("userId"); if (userId) { - await checkPaymentFromPB(userId); + const found = await checkPaymentFromPB(userId); + if (found) { + setUserName(getStorage("userName")); + } } setLoading(false); }; @@ -379,6 +348,8 @@ export default function Home() { } }, [paidType]); + const isLoggedIn = paidType === "vip"; + if (loading) { return (
@@ -388,124 +359,18 @@ export default function Home() { ); } - if (FORCE_VIP_PREVIEW || paidType === "vip") { - return ( -
-
- - 🌍 异度星球 - - -
- -
-
-
-
-

欢迎加入异度星球

-

您已解锁全部 VIP 权益

-
-
- 星球地址 - qun.hackrobot.cn -
-
- 账号 - {getStorage("userName") || (FORCE_VIP_PREVIEW ? "preview" : "")} - -
-
- 密码 - 123456 -
- - 进入星球 → - -

打开网页后,请点击「左上角头像」打开侧面栏

-
-
-
-
-
- ); - } - return ( -
-
- - 🌍 异度星球 - - -
- -
-
-

🎒 数字游民进阶社群 · 私密资源

-

- 异度星球 -

-

- 解锁电子书、视频教程、即时问答 · 抱团出海,技术杠杆 -

-
- -
-
- {FEATURES.map((f, i) => ( -
- {f.icon} -

{f.title}

-

{f.desc}

-
- ))} -
-
- -
-

🔓 解锁 VIP

-

- 一次付费·全部解锁 -

-
- {UNLOCK_ITEMS.map((item, i) => ( -
- {item.icon} - {item.title} - {item.desc} -
- ))} -
-
- -
-
-

限时特惠 · {formatRestoreDateShort()}恢复原价

-

阶梯定价 · 数字交付 · 不支持退款

-

🚫 不适合躺赚、现金流紧张

- -
-
-
-
+ + {isLoggedIn ? ( + + ) : ( + + )} + ); } diff --git a/app/topic/[topicId]/layout.tsx b/app/topic/[topicId]/layout.tsx new file mode 100644 index 0000000..16961e9 --- /dev/null +++ b/app/topic/[topicId]/layout.tsx @@ -0,0 +1,27 @@ +import type { Metadata } from "next"; + +export async function generateMetadata({ + params, +}: { + params: Promise<{ topicId: string }>; +}): Promise { + const { topicId } = await params; + const titles: Record = { + cloudphone: "云手机模块 | 低成本自建云手机", + livestream: "无人直播模块 | TikTok/YouTube 24小时自动化", + indie: "独立开发模块 | 即将上线", + aiagent: "AI Agent 模块 | 即将上线", + }; + return { + title: titles[topicId] ?? "模块 | 异度星球", + description: "异度星球 VIP 会员专属视频教程模块", + }; +} + +export default function TopicLayout({ + children, +}: { + children: React.ReactNode; +}) { + return <>{children}; +} diff --git a/app/topic/[topicId]/page.tsx b/app/topic/[topicId]/page.tsx new file mode 100644 index 0000000..497340b --- /dev/null +++ b/app/topic/[topicId]/page.tsx @@ -0,0 +1,54 @@ +"use client"; + +import { useParams } from "next/navigation"; +import Link from "next/link"; +import { TopicHeader } from "../components/TopicHeader"; +import { TopicBreadcrumb } from "../components/TopicBreadcrumb"; +import { TopicHero } from "../components/TopicHero"; +import { TopicCurriculum } from "../components/TopicCurriculum"; +import { TopicComingSoon } from "../components/TopicComingSoon"; +import { TopicFooter } from "../components/TopicFooter"; +import { getTopicConfig } from "../config"; +import { ChatFloatButton } from "@/app/cloudphone/components/ChatFloatButton"; + +export default function TopicPage() { + const params = useParams(); + const topicId = String(params?.topicId ?? ""); + const config = getTopicConfig(topicId); + + if (!config) { + return ( +
+ +
+
+

模块不存在

+

请检查链接或返回首页

+ + ← 返回首页 + +
+
+
+ ); + } + + return ( +
+ + +
+
+ +
+ + + + +
+
+ ); +} diff --git a/app/topic/components/TopicBreadcrumb.tsx b/app/topic/components/TopicBreadcrumb.tsx new file mode 100644 index 0000000..2ce7f7c --- /dev/null +++ b/app/topic/components/TopicBreadcrumb.tsx @@ -0,0 +1,30 @@ +"use client"; + +import Link from "next/link"; +import { getTopicConfig } from "../config"; + +type TopicBreadcrumbProps = { + topicId: string; +}; + +export function TopicBreadcrumb({ topicId }: TopicBreadcrumbProps) { + const config = getTopicConfig(topicId); + if (!config) return null; + + return ( + + ); +} diff --git a/app/topic/components/TopicComingSoon.tsx b/app/topic/components/TopicComingSoon.tsx new file mode 100644 index 0000000..e035895 --- /dev/null +++ b/app/topic/components/TopicComingSoon.tsx @@ -0,0 +1,31 @@ +"use client"; + +import Link from "next/link"; +import { getTopicConfig } from "../config"; + +type TopicComingSoonProps = { + topicId: string; +}; + +export function TopicComingSoon({ topicId }: TopicComingSoonProps) { + const config = getTopicConfig(topicId); + if (!config?.externalUrl) return null; + + return ( +
+
+
{config.icon}
+

即将上线

+

{config.desc}

+ + 前往数字游民指南了解更多 → + +
+
+ ); +} diff --git a/app/topic/components/TopicCurriculum.tsx b/app/topic/components/TopicCurriculum.tsx new file mode 100644 index 0000000..864aee7 --- /dev/null +++ b/app/topic/components/TopicCurriculum.tsx @@ -0,0 +1,152 @@ +"use client"; + +import { useState, useEffect } from "react"; +import Link from "next/link"; +import { getTopicCurriculum } from "../config"; +import { canWatchLesson } from "@/app/cloudphone/lib/payment"; +import { isBookmarked, toggleBookmark, getBookmarks, getCompletedLessons } from "@/app/cloudphone/lib/learning"; + +type TopicCurriculumProps = { + topicId: string; +}; + +export function TopicCurriculum({ topicId }: TopicCurriculumProps) { + const [expanded, setExpanded] = useState>({}); + const [bookmarks, setBookmarks] = useState>(new Set()); + const [completed, setCompleted] = useState>(new Set()); + + const curriculum = getTopicCurriculum(topicId); + + useEffect(() => { + setBookmarks(getBookmarks()); + setCompleted(getCompletedLessons()); + const onB = () => setBookmarks(getBookmarks()); + const onP = () => setCompleted(getCompletedLessons()); + window.addEventListener("learning:bookmarks", onB); + window.addEventListener("learning:progress", onP); + return () => { + window.removeEventListener("learning:bookmarks", onB); + window.removeEventListener("learning:progress", onP); + }; + }, []); + + useEffect(() => { + const curr = getTopicCurriculum(topicId); + if (curr?.modules?.length) { + setExpanded((prev) => { + const next = { ...prev }; + curr.modules.forEach((_, i) => { + if (next[i] === undefined) next[i] = i === 0; + }); + return next; + }); + } + }, [topicId]); + + const toggle = (i: number) => { + setExpanded((prev) => ({ ...prev, [i]: !prev[i] })); + }; + + if (!curriculum?.hasContent || !curriculum.modules.length) return null; + + const modules = curriculum.modules; + + return ( +
+
+

实战课程

+

+ 从入门到进阶,完整学习路径 +

+
+ {modules.map((module, i) => { + const isOpen = expanded[i] ?? true; + return ( +
+ + {isOpen && ( +
    + {module.lessons.map((lesson, li) => { + const origMod = module.originalModuleIndex; + const free = origMod === 0 && li < 2; + const unlocked = canWatchLesson(origMod, li); + const key = `${origMod}-${li}`; + const bookmarked = isBookmarked(origMod, li); + const isCompleted = completed.has(key); + return ( +
  • + + + + + {lesson.name} + + {free && ( + + 试看 + + )} + {isCompleted && ( + + ✓ + + )} + {!unlocked && ( + 🔒 + )} + + + {lesson.duration} + + + +
  • + ); + })} +
+ )} +
+ ); + })} +
+
+
+ ); +} diff --git a/app/topic/components/TopicFooter.tsx b/app/topic/components/TopicFooter.tsx new file mode 100644 index 0000000..0290e78 --- /dev/null +++ b/app/topic/components/TopicFooter.tsx @@ -0,0 +1,44 @@ +"use client"; + +import Link from "next/link"; +import { getTopicConfig } from "../config"; + +type TopicFooterProps = { + topicId: string; +}; + +export function TopicFooter({ topicId }: TopicFooterProps) { + const config = getTopicConfig(topicId); + + return ( +
+
+
+ + 返回首页 + + + 模块列表 + + + + + + 即时问答 + +
+
+ {config ? `${config.icon} ${config.title} · 异度星球` : "异度星球 · 数字游民社群"} +
+
+
+ ); +} diff --git a/app/topic/components/TopicHeader.tsx b/app/topic/components/TopicHeader.tsx new file mode 100644 index 0000000..e16d752 --- /dev/null +++ b/app/topic/components/TopicHeader.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { useState, useEffect } from "react"; +import Link from "next/link"; +import { useParams } from "next/navigation"; +import { ThemeToggle } from "@/app/components/ThemeToggle"; +import { getTopicConfig } from "../config"; +import { isPaid } from "@/app/cloudphone/lib/payment"; + +type TopicHeaderProps = { + topicId: string; +}; + +export function TopicHeader({ topicId }: TopicHeaderProps) { + const [paid, setPaid] = useState(false); + const config = getTopicConfig(topicId); + + useEffect(() => { + setPaid(isPaid()); + const onPayUpdate = () => setPaid(isPaid()); + window.addEventListener("pay:updated", onPayUpdate); + return () => window.removeEventListener("pay:updated", onPayUpdate); + }, []); + + const ctaHref = config?.externalUrl + ? config.externalUrl + : paid + ? "/cloudphone/course/0/0" + : config?.payUrl ?? "/"; + + return ( +
+
+ + 🌍 异度星球 + + +
+
+ ); +} diff --git a/app/topic/components/TopicHero.tsx b/app/topic/components/TopicHero.tsx new file mode 100644 index 0000000..e3020a5 --- /dev/null +++ b/app/topic/components/TopicHero.tsx @@ -0,0 +1,114 @@ +"use client"; + +import { useState, useEffect } from "react"; +import Link from "next/link"; +import { getTopicConfig, getTopicCurriculum } from "../config"; +import { getLastWatched, getProgress } from "@/app/cloudphone/lib/learning"; +import { ParticleBackground } from "@/app/cloudphone/components/ParticleBackground"; + +type TopicHeroProps = { + topicId: string; +}; + +export function TopicHero({ topicId }: TopicHeroProps) { + const config = getTopicConfig(topicId); + const curriculum = getTopicCurriculum(topicId); + const [last, setLast] = useState>(null); + const [progress, setProgress] = useState({ completed: 0, percent: 0 }); + + const totalLessons = curriculum?.modules.reduce((s, m) => s + m.lessons.length, 0) ?? 0; + + useEffect(() => { + if (curriculum?.hasContent && totalLessons > 0) { + setLast(getLastWatched()); + setProgress(getProgress(totalLessons)); + const onProgress = () => { + setLast(getLastWatched()); + setProgress(getProgress(totalLessons)); + }; + window.addEventListener("learning:progress", onProgress); + return () => window.removeEventListener("learning:progress", onProgress); + } + }, [curriculum?.hasContent, totalLessons]); + + if (!config) return null; + + const ctaHref = config.externalUrl + ? config.externalUrl + : last + ? `/cloudphone/course/${last.moduleIndex}/${last.lessonIndex}` + : config.payUrl; + + return ( +
+
+ +
+

+ {config.icon} {config.title} +

+

+ {config.subtitle} +

+ {config.desc && ( +

+ {config.desc} +

+ )} +
+ {config.stats.map((s) => ( +
+
+ {s.value} +
+
{s.label}
+
+ ))} +
+ {curriculum?.hasContent && ( +
+ + {config.externalUrl ? "前往了解 →" : last ? "继续学习 →" : "立即报名 →"} + +
+ )} + {curriculum?.hasContent && progress.completed > 0 && ( +
+
+ 学习进度 + + {progress.completed}/{totalLessons} 节 + +
+
+
+
+
+ )} +
+
+ ); +} diff --git a/app/topic/config.ts b/app/topic/config.ts new file mode 100644 index 0000000..fe40ef5 --- /dev/null +++ b/app/topic/config.ts @@ -0,0 +1,92 @@ +/** + * 模块配置 - 模块 > 视频教程 > [模块名] + * 参考 cloudphone/nomadlms 结构 + */ + +import { CURRICULUM_CONFIG } from "@/app/cloudphone/config/course"; + +export type TopicConfig = { + id: string; + icon: string; + title: string; + subtitle: string; + desc: string; + stats: { value: string; label: string }[]; + /** 使用的 curriculum 模块索引,空则用全部 */ + moduleIndices?: number[]; + /** 外部链接,有则显示「前往了解」而非课程列表 */ + externalUrl?: string; + payUrl: string; +}; + +export const TOPIC_CONFIGS: Record = { + cloudphone: { + id: "cloudphone", + icon: "☁️", + title: "云手机模块", + subtitle: "低成本自建云手机,TikTok/YouTube 运营搭建指南", + desc: "用开源硬件打造云手机,Redroid、AOSP、ffmpeg、SRS,从零搭建无人直播工作室", + stats: [ + { value: "13", label: "节实战课程" }, + { value: "170+", label: "分钟视频" }, + { value: "5", label: "大模块" }, + ], + payUrl: "/cloudphone/pay", + }, + livestream: { + id: "livestream", + icon: "📺", + title: "无人直播模块", + subtitle: "TikTok/YouTube 无人直播,24 小时自动化", + desc: "ffmpeg 拉流转推、SRS 直播服务器、油猴脚本自动化,实现 24 小时无人值守推流", + stats: [ + { value: "7", label: "节核心课程" }, + { value: "100+", label: "分钟视频" }, + { value: "2", label: "大模块" }, + ], + moduleIndices: [1, 2], + payUrl: "/cloudphone/pay", + }, + indie: { + id: "indie", + icon: "🛠️", + title: "独立开发模块", + subtitle: "从想法到产品,独立开发者全流程指南", + desc: "即将上线,敬请期待。可先前往数字游民指南了解更多。", + stats: [ + { value: "—", label: "课程筹备中" }, + { value: "—", label: "敬请期待" }, + ], + externalUrl: "https://digital.hackrobot.cn/zh", + payUrl: "/", + }, + aiagent: { + id: "aiagent", + icon: "🤖", + title: "AI Agent 模块", + subtitle: "AIGC 与智能体,Vibe Coding 跨界创造", + desc: "即将上线,敬请期待。可先前往数字游民指南了解更多。", + stats: [ + { value: "—", label: "课程筹备中" }, + { value: "—", label: "敬请期待" }, + ], + externalUrl: "https://digital.hackrobot.cn/zh", + payUrl: "/", + }, +}; + +export function getTopicConfig(topicId: string): TopicConfig | null { + return TOPIC_CONFIGS[topicId] ?? null; +} + +export function getTopicCurriculum(topicId: string) { + const config = getTopicConfig(topicId); + if (!config) return null; + if (config.externalUrl) return { modules: [], hasContent: false }; + const indices = config.moduleIndices ?? CURRICULUM_CONFIG.modules.map((_, i) => i); + const modules = indices + .map((i) => ({ module: CURRICULUM_CONFIG.modules[i], originalIndex: i })) + .filter((x) => x.module) + .map(({ module: m, originalIndex }) => ({ ...m, originalModuleIndex: originalIndex })); + return { modules, hasContent: modules.length > 0 }; +} diff --git a/app/vip/components/DashboardPage.tsx b/app/vip/components/DashboardPage.tsx new file mode 100644 index 0000000..e6fe818 --- /dev/null +++ b/app/vip/components/DashboardPage.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { WelcomeBlock } from "./dashboard/WelcomeBlock"; +import { UnlockedStats } from "./dashboard/UnlockedStats"; +import { RecentUpdates } from "./dashboard/RecentUpdates"; +import { ContinueLearning } from "./dashboard/ContinueLearning"; +import { TopicEbookShowcase } from "./landing/TopicEbookShowcase"; +import { CommunityHighlights } from "./dashboard/CommunityHighlights"; +import { WeeklyActions } from "./dashboard/WeeklyActions"; + +type DashboardPageProps = { + userName: string | null; +}; + +export function DashboardPage({ userName }: DashboardPageProps) { + return ( + <> + + + + + + + + + ); +} diff --git a/app/vip/components/LandingPage.tsx b/app/vip/components/LandingPage.tsx new file mode 100644 index 0000000..38b795e --- /dev/null +++ b/app/vip/components/LandingPage.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { LandingHero } from "./landing/LandingHero"; +import { AssetStats } from "./landing/AssetStats"; +import { TopicEbookShowcase } from "./landing/TopicEbookShowcase"; +import { CompareSection } from "./landing/CompareSection"; +import { CommunityPreview } from "./landing/CommunityPreview"; +import { FAQSection } from "./landing/FAQSection"; +import { LandingCTA } from "./landing/LandingCTA"; +import { LoginBlock } from "./landing/LoginBlock"; + +type LandingPageProps = { + onJoin: () => void; + onLogin: () => void; + onVerify: (userId: string) => Promise; + onVerifySuccess: () => void; +}; + +export function LandingPage({ + onJoin, + onLogin, + onVerify, + onVerifySuccess, +}: LandingPageProps) { + return ( + <> + + + + + + + + + + ); +} diff --git a/app/vip/components/VipHeader.tsx b/app/vip/components/VipHeader.tsx new file mode 100644 index 0000000..74eb7a3 --- /dev/null +++ b/app/vip/components/VipHeader.tsx @@ -0,0 +1,35 @@ +"use client"; + +import Link from "next/link"; +import { ThemeToggle } from "@/app/components/ThemeToggle"; +import styles from "./vip.module.css"; + +type VipHeaderProps = { + isLoggedIn: boolean; + userName?: string | null; +}; + +export function VipHeader({ isLoggedIn, userName }: VipHeaderProps) { + return ( +
+ + 🌍 异度星球 + + +
+ ); +} diff --git a/app/vip/components/VipLayout.tsx b/app/vip/components/VipLayout.tsx new file mode 100644 index 0000000..2a76e08 --- /dev/null +++ b/app/vip/components/VipLayout.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { type ReactNode } from "react"; +import { VipHeader } from "./VipHeader"; +import styles from "./vip.module.css"; + +type VipLayoutProps = { + children: ReactNode; + isLoggedIn: boolean; + userName?: string | null; +}; + +export function VipLayout({ children, isLoggedIn, userName }: VipLayoutProps) { + return ( +
+ +
{children}
+
+ ); +} diff --git a/app/vip/components/dashboard/CommunityHighlights.tsx b/app/vip/components/dashboard/CommunityHighlights.tsx new file mode 100644 index 0000000..91283bd --- /dev/null +++ b/app/vip/components/dashboard/CommunityHighlights.tsx @@ -0,0 +1,38 @@ +"use client"; + +import Link from "next/link"; +import { COMMUNITY_HIGHLIGHTS } from "@/app/data/vip.mock"; +import styles from "../vip.module.css"; + +export function CommunityHighlights() { + return ( +
+

社区精选

+
+ {COMMUNITY_HIGHLIGHTS.map((item) => ( + + {item.text} + + → + + + ))} +
+
+ ); +} diff --git a/app/vip/components/dashboard/ContinueLearning.tsx b/app/vip/components/dashboard/ContinueLearning.tsx new file mode 100644 index 0000000..19a69a1 --- /dev/null +++ b/app/vip/components/dashboard/ContinueLearning.tsx @@ -0,0 +1,100 @@ +"use client"; + +import Link from "next/link"; +import { getLastWatched, getProgress } from "@/app/cloudphone/lib/learning"; +import { CURRICULUM_CONFIG } from "@/app/cloudphone/config/course"; +import { useState, useEffect } from "react"; +import styles from "../vip.module.css"; + +const TOTAL_LESSONS = CURRICULUM_CONFIG.modules.reduce((s, m) => s + m.lessons.length, 0); + +export function ContinueLearning() { + const [last, setLast] = useState>(null); + const [progress, setProgress] = useState({ completed: 0, percent: 0 }); + + useEffect(() => { + setLast(getLastWatched()); + setProgress(getProgress(TOTAL_LESSONS)); + const onProgress = () => { + setLast(getLastWatched()); + setProgress(getProgress(TOTAL_LESSONS)); + }; + window.addEventListener("learning:progress", onProgress); + return () => window.removeEventListener("learning:progress", onProgress); + }, []); + + if (!last || progress.completed >= TOTAL_LESSONS) { + return ( +
+

继续学习

+ + {progress.completed >= TOTAL_LESSONS ? "复习课程" : "开始学习"} → + +
+ ); + } + + const module = CURRICULUM_CONFIG.modules[last.moduleIndex]; + const lesson = module?.lessons[last.lessonIndex]; + const title = lesson?.name || `第 ${last.moduleIndex + 1} 章`; + + return ( +
+

继续学习

+ +
+ {title} +
+
+
+ 进度 + + {progress.completed}/{TOTAL_LESSONS} 节 + +
+
+
+
+
+
+ 继续学习 → +
+ +
+ ); +} diff --git a/app/vip/components/dashboard/RecentUpdates.tsx b/app/vip/components/dashboard/RecentUpdates.tsx new file mode 100644 index 0000000..faa4742 --- /dev/null +++ b/app/vip/components/dashboard/RecentUpdates.tsx @@ -0,0 +1,44 @@ +"use client"; + +import Link from "next/link"; +import { RECENT_UPDATES } from "@/app/data/vip.mock"; +import styles from "../vip.module.css"; + +export function RecentUpdates() { + return ( +
+

最近更新

+
+ {RECENT_UPDATES.map((item) => { + const content = ( +
+ {item.title} + + {item.date} + +
+ ); + if (item.href) { + return ( + + {content} + + ); + } + return
{content}
; + })} +
+
+ ); +} diff --git a/app/vip/components/dashboard/TopicCards.tsx b/app/vip/components/dashboard/TopicCards.tsx new file mode 100644 index 0000000..7c375eb --- /dev/null +++ b/app/vip/components/dashboard/TopicCards.tsx @@ -0,0 +1,45 @@ +"use client"; + +import Link from "next/link"; +import { TOPICS } from "@/app/data/vip.mock"; +import styles from "../vip.module.css"; + +export function TopicCards() { + return ( +
+

模块入口

+
+ {TOPICS.map((t) => { + const content = ( + <> + {t.icon} + {t.title} + {t.desc} + + ); + if (t.href) { + return ( + + {content} + + ); + } + return ( +
+ {content} +
+ ); + })} +
+
+ ); +} diff --git a/app/vip/components/dashboard/UnlockedStats.tsx b/app/vip/components/dashboard/UnlockedStats.tsx new file mode 100644 index 0000000..44d0071 --- /dev/null +++ b/app/vip/components/dashboard/UnlockedStats.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { ASSET_STATS } from "@/app/data/vip.mock"; +import styles from "../vip.module.css"; + +const STAT_ITEMS = [ + { key: "topics", label: "已解锁模块" }, + { key: "ebooks", label: "电子书" }, + { key: "videos", label: "视频" }, + { key: "templates", label: "模板" }, +] as const; + +export function UnlockedStats() { + return ( +
+

已解锁

+
+ {STAT_ITEMS.map(({ key, label }) => { + const value = String(ASSET_STATS[key as keyof typeof ASSET_STATS]); + return ( +
+
{value}
+
{label}
+
+ ); + })} +
+
+ ); +} diff --git a/app/vip/components/dashboard/WeeklyActions.tsx b/app/vip/components/dashboard/WeeklyActions.tsx new file mode 100644 index 0000000..30f4df7 --- /dev/null +++ b/app/vip/components/dashboard/WeeklyActions.tsx @@ -0,0 +1,56 @@ +"use client"; + +import Link from "next/link"; +import { WEEKLY_ACTIONS } from "@/app/data/vip.mock"; +import styles from "../vip.module.css"; + +export function WeeklyActions() { + return ( +
+

本周推荐

+
+ {WEEKLY_ACTIONS.map((item) => ( + + + {item.priority} + + {item.text} + + → + + + ))} +
+
+ ); +} diff --git a/app/vip/components/dashboard/WelcomeBlock.tsx b/app/vip/components/dashboard/WelcomeBlock.tsx new file mode 100644 index 0000000..0c874f6 --- /dev/null +++ b/app/vip/components/dashboard/WelcomeBlock.tsx @@ -0,0 +1,62 @@ +"use client"; + +import Link from "next/link"; +import styles from "../vip.module.css"; + +type WelcomeBlockProps = { + userName: string | null; +}; + +function getStorage(key: string): string | null { + if (typeof window === "undefined") return null; + return localStorage.getItem(key); +} + +export function WelcomeBlock({ userName }: WelcomeBlockProps) { + const displayName = userName || getStorage("userName") || "会员"; + return ( +
+

+ 欢迎回来,{displayName} +

+

+ 以下是您的会员资产与推荐动作 +

+ + 🌐 进入星球 qun.hackrobot.cn → + +
+ ); +} diff --git a/app/vip/components/landing/AssetStats.tsx b/app/vip/components/landing/AssetStats.tsx new file mode 100644 index 0000000..d272d1b --- /dev/null +++ b/app/vip/components/landing/AssetStats.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { ASSET_STATS } from "@/app/data/vip.mock"; +import styles from "../vip.module.css"; + +const STAT_ITEMS = [ + { key: "topics", label: "模块" }, + { key: "ebooks", label: "电子书" }, + { key: "videos", label: "视频" }, + { key: "templates", label: "模板" }, + { key: "updateFrequency", label: "更新" }, +] as const; + +export function AssetStats() { + return ( +
+
+ {STAT_ITEMS.map(({ key, label }, i) => { + const value = + key === "updateFrequency" + ? ASSET_STATS[key] + : String(ASSET_STATS[key as keyof typeof ASSET_STATS]); + return ( +
+
{value}
+
{label}
+
+ ); + })} +
+
+ ); +} diff --git a/app/vip/components/landing/CommunityPreview.tsx b/app/vip/components/landing/CommunityPreview.tsx new file mode 100644 index 0000000..0427b45 --- /dev/null +++ b/app/vip/components/landing/CommunityPreview.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { COMMUNITY_PREVIEW } from "@/app/data/vip.mock"; +import styles from "../vip.module.css"; + +export function CommunityPreview() { + return ( +
+

社区价值

+
+ {COMMUNITY_PREVIEW.map((item, i) => ( +
+ {item.icon} + {item.text} +
+ ))} +
+
+ ); +} diff --git a/app/vip/components/landing/CompareSection.tsx b/app/vip/components/landing/CompareSection.tsx new file mode 100644 index 0000000..b70ddb6 --- /dev/null +++ b/app/vip/components/landing/CompareSection.tsx @@ -0,0 +1,87 @@ +"use client"; + +import { COMPARE_ITEMS } from "@/app/data/vip.mock"; +import styles from "../vip.module.css"; + +export function CompareSection() { + return ( +
+

单品 vs 会员

+

一次付费,全部解锁

+
+ + + + + + + + + + {COMPARE_ITEMS.map((row, i) => ( + + + + + + ))} + +
+ 权益 + + 单品 + + 会员 +
+ {row.feature} + + {row.single} + + {row.member} +
+
+
+ ); +} diff --git a/app/vip/components/landing/FAQSection.tsx b/app/vip/components/landing/FAQSection.tsx new file mode 100644 index 0000000..7cf4950 --- /dev/null +++ b/app/vip/components/landing/FAQSection.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { useState } from "react"; +import { FAQ_ITEMS } from "@/app/data/vip.mock"; +import styles from "../vip.module.css"; + +export function FAQSection() { + const [openIndex, setOpenIndex] = useState(null); + + return ( +
+

常见问题

+
+ {FAQ_ITEMS.map((item, i) => ( +
setOpenIndex(openIndex === i ? null : i)} + > +
+ {item.q} + + ▼ + +
+ {openIndex === i && ( +

+ {item.a} +

+ )} +
+ ))} +
+
+ ); +} diff --git a/app/vip/components/landing/LandingCTA.tsx b/app/vip/components/landing/LandingCTA.tsx new file mode 100644 index 0000000..aae7953 --- /dev/null +++ b/app/vip/components/landing/LandingCTA.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { formatRestoreDateShort } from "@/config/promo.config"; +import styles from "../vip.module.css"; + +type LandingCTAProps = { + onJoin: () => void; + onLogin: () => void; +}; + +export function LandingCTA({ onJoin, onLogin }: LandingCTAProps) { + return ( +
+
+

+ 限时特惠 · {formatRestoreDateShort()}恢复原价 +

+

+ 阶梯定价 · 数字交付 · 不支持退款 +

+

+ 🚫 不适合躺赚、现金流紧张 +

+
+ + +
+
+
+ ); +} diff --git a/app/vip/components/landing/LandingHero.tsx b/app/vip/components/landing/LandingHero.tsx new file mode 100644 index 0000000..74e0354 --- /dev/null +++ b/app/vip/components/landing/LandingHero.tsx @@ -0,0 +1,55 @@ +"use client"; + +import styles from "../vip.module.css"; + +export function LandingHero() { + return ( +
+

+ 异度星球会员中心 +

+

+ 全模块解锁 · 持续更新 · 抱团出海 +

+

+ 一次付费,解锁电子书、视频教程、私密社群、即时问答,享受每周更新 +

+
+ ); +} diff --git a/app/vip/components/landing/LoginBlock.tsx b/app/vip/components/landing/LoginBlock.tsx new file mode 100644 index 0000000..3863a9c --- /dev/null +++ b/app/vip/components/landing/LoginBlock.tsx @@ -0,0 +1,90 @@ +"use client"; + +import { useState, useRef, useEffect } from "react"; +import styles from "../vip.module.css"; + +type LoginBlockProps = { + onVerify: (userId: string) => Promise; + onSuccess: () => void; +}; + +export function LoginBlock({ onVerify, onSuccess }: LoginBlockProps) { + const [userId, setUserId] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const inputRef = useRef(null); + + useEffect(() => { + const hash = typeof window !== "undefined" ? window.location.hash : ""; + if (hash === "#login") { + inputRef.current?.focus(); + } + }, []); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const trimmed = userId.trim(); + if (!trimmed) { + setError("请输入您的账号"); + return; + } + setError(null); + setLoading(true); + try { + const ok = await onVerify(trimmed); + if (ok) { + onSuccess(); + } else { + setError("未找到该账号的会员记录,请确认账号正确"); + } + } catch { + setError("验证失败,请稍后重试"); + } finally { + setLoading(false); + } + }; + + return ( +
+
+

已有账号登录

+

+ 请输入您的会员账号(支付时使用的 user_id)以恢复权益 +

+
+ setUserId(e.target.value)} + placeholder="例如:user1234567890" + disabled={loading} + style={{ + width: "100%", + padding: "0.75rem 1rem", + fontSize: "1rem", + border: "1px solid var(--border)", + borderRadius: "8px", + background: "var(--background)", + color: "var(--foreground)", + marginBottom: "0.75rem", + }} + /> + {error && ( +

+ {error} +

+ )} + +
+
+
+ ); +} diff --git a/app/vip/components/landing/TopicEbookShowcase.tsx b/app/vip/components/landing/TopicEbookShowcase.tsx new file mode 100644 index 0000000..2c288d6 --- /dev/null +++ b/app/vip/components/landing/TopicEbookShowcase.tsx @@ -0,0 +1,103 @@ +"use client"; + +import Link from "next/link"; +import { TOPICS_DISPLAY, EBOOKS_DISPLAY, VIDEO_COURSES_DISPLAY } from "@/app/data/vip.mock"; +import styles from "../vip.module.css"; + +export function TopicEbookShowcase() { + return ( +
+ {/* 模块展示 */} +

模块

+
+ {TOPICS_DISPLAY.map((t) => { + const content = ( + <> + {t.icon} + {t.title} + {t.desc} + + ); + if (t.href) { + return ( + + {content} + + ); + } + return ( +
+ {content} +
+ ); + })} +
+ + {/* 视频教程展示 - 参考 digital 资源导航,至少 4 个 */} +

视频教程

+
+ {VIDEO_COURSES_DISPLAY.map((v) => ( + + {v.icon} + {v.title} + {v.desc} + + ))} +
+ + {/* 电子书展示 */} +

电子书

+
+ {EBOOKS_DISPLAY.map((e) => ( + + 📖 +
+ 《{e.title}》 +
+
{e.desc}
+
+ 阅读 → +
+ + ))} +
+
+ ); +} diff --git a/app/vip/components/landing/TopicEntries.tsx b/app/vip/components/landing/TopicEntries.tsx new file mode 100644 index 0000000..50b5648 --- /dev/null +++ b/app/vip/components/landing/TopicEntries.tsx @@ -0,0 +1,45 @@ +"use client"; + +import Link from "next/link"; +import { TOPICS } from "@/app/data/vip.mock"; +import styles from "../vip.module.css"; + +export function TopicEntries() { + return ( +
+

核心模块

+
+ {TOPICS.map((t, i) => { + const content = ( + <> + {t.icon} + {t.title} + {t.desc} + + ); + if (t.href) { + return ( + + {content} + + ); + } + return ( +
+ {content} +
+ ); + })} +
+
+ ); +} diff --git a/app/vip/components/vip.module.css b/app/vip/components/vip.module.css new file mode 100644 index 0000000..54dac01 --- /dev/null +++ b/app/vip/components/vip.module.css @@ -0,0 +1,264 @@ +/* VIP 会员中心 - 深色主题优先,会员感、控制台感、资产感 */ + +.root { + min-height: 100vh; + background: var(--background); + color: var(--foreground); +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1.5rem; + max-width: 1200px; + margin: 0 auto; +} + +@media (max-width: 640px) { + .header { + padding: 0.75rem 1rem; + } +} + +.logo { + font-size: 1.25rem; + font-weight: 700; + color: var(--foreground); + text-decoration: none; +} + +.logo:hover { + color: var(--accent); +} + +.nav { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.loginLink { + font-size: 0.9rem; + color: var(--muted-foreground); + text-decoration: none; +} + +.loginLink:hover { + color: var(--accent); +} + +.userBadge { + font-size: 0.875rem; + color: var(--accent); + font-weight: 500; +} + +.main { + max-width: 1200px; + margin: 0 auto; + padding: 0 1.5rem 2rem; +} + +@media (max-width: 640px) { + .main { + padding: 0 1rem 1.5rem; + } +} + +/* 区块通用 */ +.section { + padding: 1.5rem 0; +} + +.sectionTitle { + font-size: 1.25rem; + font-weight: 600; + color: var(--foreground); + margin: 0 0 1rem; +} + +.sectionDesc { + font-size: 0.9rem; + color: var(--muted-foreground); + margin: 0 0 1rem; + line-height: 1.5; +} + +/* 卡片 */ +.card { + background: var(--card); + border: 1px solid var(--border); + border-radius: 12px; + padding: 1rem 1.25rem; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.card:hover { + border-color: rgba(245, 158, 11, 0.3); +} + +/* 大数字 */ +.bigNumber { + font-size: 2rem; + font-weight: 700; + color: var(--accent); + line-height: 1.2; +} + +@media (min-width: 640px) { + .bigNumber { + font-size: 2.5rem; + } +} + +.bigNumberLabel { + font-size: 0.8rem; + color: var(--muted-foreground); + margin-top: 0.25rem; +} + +/* 按钮 */ +.btnPrimary { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.75rem 1.75rem; + font-size: 1rem; + font-weight: 600; + color: var(--accent-foreground); + background: var(--accent); + border: none; + border-radius: 12px; + cursor: pointer; + text-decoration: none; + transition: opacity 0.2s, transform 0.15s; +} + +.btnPrimary:hover { + opacity: 0.95; + transform: translateY(-1px); +} + +.btnSecondary { + display: inline-flex; + align-items: center; + padding: 0.75rem 1.5rem; + font-size: 0.95rem; + font-weight: 500; + color: var(--muted-foreground); + background: transparent; + border: 1px solid var(--border); + border-radius: 12px; + text-decoration: none; + cursor: pointer; + transition: border-color 0.15s, color 0.15s; +} + +.btnSecondary:hover { + border-color: var(--accent); + color: var(--accent); +} + +.btnLarge { + padding: 1rem 2.5rem; + font-size: 1.1rem; +} + +/* 资产统计网格 */ +.statsGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; +} + +@media (min-width: 640px) { + .statsGrid { + grid-template-columns: repeat(5, 1fr); + } +} + +.statsGrid4 { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; +} + +@media (min-width: 640px) { + .statsGrid4 { + grid-template-columns: repeat(4, 1fr); + } +} + +/* 模块网格 */ +.topicGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; +} + +@media (min-width: 640px) { + .topicGrid { + grid-template-columns: repeat(4, 1fr); + } +} + +.topicCard { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + padding: 1.25rem 1rem; + background: var(--card); + border: 1px solid var(--border); + border-radius: 12px; + text-decoration: none; + color: inherit; + transition: border-color 0.2s, transform 0.2s; +} + +.topicCard:hover { + border-color: rgba(245, 158, 11, 0.4); + transform: translateY(-2px); +} + +.topicIcon { + font-size: 2rem; + line-height: 1; +} + +.topicTitle { + font-size: 1rem; + font-weight: 600; + color: var(--foreground); +} + +.topicDesc { + font-size: 0.8rem; + color: var(--muted-foreground); +} + +/* Loading */ +.loading { + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1rem; + background: var(--background); + color: var(--foreground); +} + +.loadingSpinner { + width: 40px; + height: 40px; + border: 3px solid var(--border); + border-top-color: var(--accent); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} diff --git a/docs/API_FIELD_MAPPING.md b/docs/API_FIELD_MAPPING.md new file mode 100644 index 0000000..c13737d --- /dev/null +++ b/docs/API_FIELD_MAPPING.md @@ -0,0 +1,155 @@ +# VIP 首页 API 字段映射建议 + +后端默认:`api.hackrobot.cn`。以下为 Mock 数据与真实 API 的字段映射建议,便于后续接入。 + +--- + +## 1. 资产统计 (ASSET_STATS) + +**Mock 位置**: `app/data/vip.mock.ts` → `ASSET_STATS` + +**建议接口**: `GET /vip/stats` 或 `GET /vip/dashboard/summary` + +| 前端字段 | 建议 API 字段 | 说明 | +|----------|----------------|------| +| topics | `topics_count` / `topic_count` | 专题数量 | +| ebooks | `ebooks_count` | 电子书数量 | +| videos | `videos_count` | 视频数量 | +| templates | `templates_count` | 模板数量 | +| updateFrequency | `update_frequency` | 更新频率,如 "每周" | + +**响应示例**: +```json +{ + "topics_count": 4, + "ebooks_count": 2, + "videos_count": 12, + "templates_count": 8, + "update_frequency": "每周" +} +``` + +--- + +## 2. 专题入口 (TOPICS) + +**Mock 位置**: `app/data/vip.mock.ts` → `TOPICS` + +**建议接口**: `GET /vip/topics` 或复用现有内容路由配置 + +| 前端字段 | 建议 API 字段 | 说明 | +|----------|----------------|------| +| id | `id` | 专题唯一标识 | +| icon | `icon` / `emoji` | 图标或 emoji | +| title | `title` | 标题 | +| desc | `description` / `desc` | 描述 | +| href | `href` / `url` / `path` | 链接,null 表示未开放 | + +--- + +## 3. 权益对比 (COMPARE_ITEMS) + +**Mock 位置**: `app/data/vip.mock.ts` → `COMPARE_ITEMS` + +**建议**: 可配置化,接口 `GET /vip/compare` 或静态配置 + +| 前端字段 | 建议 API 字段 | +|----------|----------------| +| feature | `feature` | +| single | `single_label` | +| member | `member_label` | + +--- + +## 4. 最近更新 (RECENT_UPDATES) - 已登录 + +**Mock 位置**: `app/data/vip.mock.ts` → `RECENT_UPDATES` + +**建议接口**: `GET /vip/recent-updates` 或 `GET /vip/dashboard/recent` + +| 前端字段 | 建议 API 字段 | 说明 | +|----------|----------------|------| +| id | `id` | 记录 ID | +| title | `title` | 更新标题 | +| date | `updated_at` / `date` | 日期,建议 ISO 或 "MM月DD日" | +| href | `href` / `url` | 跳转链接 | + +**需鉴权**: 需携带 `user_id` 或 session/cookie + +--- + +## 5. 社区精选 (COMMUNITY_HIGHLIGHTS) - 已登录 + +**Mock 位置**: `app/data/vip.mock.ts` → `COMMUNITY_HIGHLIGHTS` + +**建议接口**: `GET /vip/community/highlights` + +| 前端字段 | 建议 API 字段 | +|----------|----------------| +| id | `id` | +| text | `text` / `title` | +| href | `href` / `url` | + +--- + +## 6. 本周推荐 (WEEKLY_ACTIONS) - 已登录 + +**Mock 位置**: `app/data/vip.mock.ts` → `WEEKLY_ACTIONS` + +**建议接口**: `GET /vip/weekly-actions` 或 `GET /vip/dashboard/actions` + +| 前端字段 | 建议 API 字段 | 说明 | +|----------|----------------|------| +| id | `id` | 动作 ID | +| text | `text` / `title` | 动作描述 | +| href | `href` / `url` | 跳转链接 | +| priority | `priority` / `order` | 优先级/排序 | + +--- + +## 7. 继续学习 (ContinueLearning) + +**当前实现**: 使用 `app/cloudphone/lib/learning.ts` 的 `getLastWatched()` + `getProgress()`,数据来自 localStorage。 + +**建议接口**: `GET /vip/learning/progress`(若后端统一管理学习进度) + +| 前端使用 | 建议 API 字段 | +|----------|----------------| +| last.moduleIndex | `last_module_index` | +| last.lessonIndex | `last_lesson_index` | +| progress.completed | `completed_count` | +| progress.percent | `progress_percent` | + +**鉴权**: 需 `user_id` 或 session + +--- + +## 8. 登录/会员验证 + +**当前实现**: PocketBase `payments` 集合,按 `user_id` 查询最新支付记录。 + +**兼容 future shared cookie/session**: +- 建议后端提供 `GET /vip/me` 或 `GET /auth/me`,通过 Cookie/Session 返回当前用户及会员状态 +- 响应字段建议: `user_id`, `user_name`, `is_vip`, `vip_expires_at` 等 +- 前端可优先调用此接口,失败时回退到 localStorage + PocketBase 校验 + +--- + +## 9. 统一请求封装建议 + +在 `app/lib/` 下新增 `api.ts`: + +```ts +const API_BASE = process.env.NEXT_PUBLIC_API_URL || "https://api.hackrobot.cn"; + +export async function fetchVip(path: string, options?: RequestInit): Promise { + const res = await fetch(`${API_BASE}${path}`, { + ...options, + credentials: "include", // 携带 cookie,兼容 future session + }); + if (!res.ok) throw new Error(`API error: ${res.status}`); + return res.json(); +} +``` + +后续将各 Mock 数据替换为 `fetchVip("/vip/stats")` 等调用即可。 diff --git a/docs/VIP_HOMEPAGE_IA.md b/docs/VIP_HOMEPAGE_IA.md new file mode 100644 index 0000000..af46823 --- /dev/null +++ b/docs/VIP_HOMEPAGE_IA.md @@ -0,0 +1,112 @@ +# VIP 首页信息架构 (vip.hackrobot.cn) + +## 一、页面信息架构 + +### 1. 路由结构(保持现有) + +| 路径 | 说明 | +|------|------| +| `/` | 首页(根据登录态切换 A/B 两种视图) | +| `/paid` | 支付结果页(不变) | +| `/ebook` | 电子书(不变) | +| `/cloudphone/*` | 云手机课程(不变) | + +### 2. 首页双态逻辑 + +``` +/ (首页) +├── 未登录 / 非 VIP → 会员落地页 (Landing) +└── 已登录 + VIP → 会员控制台 (Dashboard) +``` + +--- + +## 二、未登录首页 - 会员落地页 + +| 区块 | 内容 | 优先级 | +|------|------|--------| +| **Hero** | 异度星球会员中心 + 一句话价值主张 | P0 | +| **数字资产统计** | 专题数、电子书数、视频数、模板数、更新频率 | P0 | +| **四个核心专题入口** | 卡片式入口,链接到具体内容 | P0 | +| **权益对比** | 单品 vs 会员 表格/卡片 | P0 | +| **社区价值预览** | 社群、问答、沙龙等价值点 | P1 | +| **FAQ** | 常见问题折叠 | P1 | +| **CTA** | 立即加入 / 已有账号登录 | P0 | + +--- + +## 三、已登录首页 - 会员控制台 + +| 区块 | 内容 | 优先级 | +|------|------|--------| +| **欢迎区** | 用户名 + 简短问候 | P0 | +| **已解锁统计** | 专题/电子书/视频/模板 数量 | P0 | +| **最近更新** | 最新内容列表(3-5 条) | P0 | +| **继续学习** | 上次学习进度 + 跳转 | P0 | +| **四个专题卡片** | 与落地页一致,可点击进入 | P0 | +| **社区精选** | 社群/问答精选 2-3 条 | P1 | +| **本周任务/推荐动作** | 1-3 个推荐动作 | P1 | + +--- + +## 四、与 digital / meetup 的区分 + +| 站点 | 定位 | 首页气质 | +|------|------|----------| +| digital | 公开内容入口 + 单品销售 | 内容展示、购买转化 | +| meetup | 城市/活动/连接/工具 | 活动列表、城市、工具 | +| **vip** | 会员中心 + 全专题解锁 + 社区 + 持续更新 | **会员感、控制台感、资产感** | + +--- + +## 五、组件树 + +``` +Page (app/page.tsx) +├── VipLayout (共享) +│ ├── VipHeader +│ │ ├── Logo +│ │ ├── Nav (ThemeToggle, 登录/用户) +│ │ └── [可选] UserMenu +│ └── main +│ +├── [未登录] LandingPage +│ ├── LandingHero +│ ├── AssetStats +│ ├── TopicEntries (4 个专题) +│ ├── CompareSection (单品 vs 会员) +│ ├── CommunityPreview +│ ├── FAQSection +│ └── LandingCTA +│ +└── [已登录] DashboardPage + ├── WelcomeBlock + ├── UnlockedStats + ├── RecentUpdates + ├── ContinueLearning + ├── TopicCards (4 个专题) + ├── CommunityHighlights + └── WeeklyActions +``` + +--- + +## 六、组件职责 + +| 组件 | 职责 | +|------|------| +| VipLayout | 深色主题容器、Header、主内容区 | +| VipHeader | Logo、主题切换、登录/用户入口 | +| LandingHero | 标题、价值主张 | +| AssetStats | 大数字统计展示 | +| TopicEntries / TopicCards | 专题入口,复用同一数据源 | +| CompareSection | 权益对比表格 | +| CommunityPreview | 社区价值简述 | +| FAQSection | 折叠 FAQ | +| LandingCTA | 加入会员 + 登录按钮 | +| WelcomeBlock | 欢迎语 | +| UnlockedStats | 已解锁数量 | +| RecentUpdates | 最近更新列表 | +| ContinueLearning | 学习进度 + 继续按钮 | +| CommunityHighlights | 社区精选 | +| WeeklyActions | 推荐动作 | diff --git a/package.json b/package.json index a3238c9..d4d156c 100644 --- a/package.json +++ b/package.json @@ -26,5 +26,6 @@ "eslint-config-next": "16.1.6", "tailwindcss": "^4", "typescript": "^5" - } + }, + "packageManager": "pnpm@10.25.0+sha1.2cfb3ab644446565c127f58165cc76368c9c920b" }