refactor: full product interaction system redesign

Refactor homepage, reading modal, matching feature, and user profile for improved UX

#VERCEL_SKIP

Co-authored-by: undefined <undefined+undefined@users.noreply.github.com>
This commit is contained in:
v0
2026-01-14 05:17:59 +00:00
parent f3195d9331
commit 59ca3b2bbd
8 changed files with 1124 additions and 628 deletions

View File

@@ -2,177 +2,149 @@
import { useState, useEffect } from "react"
import Link from "next/link"
import { User, ShoppingBag, Share2, LogOut, ChevronRight, BookOpen, Copy, Check } from "lucide-react"
import { User, ShoppingBag, Share2, LogOut, ChevronRight, BookOpen } from "lucide-react"
import { useStore } from "@/lib/store"
import { AuthModal } from "@/components/modules/auth/auth-modal"
import { getFullBookPrice, getAllSections } from "@/lib/book-data"
import { getFullBookPrice } from "@/lib/book-data"
export default function MyPage() {
const { user, isLoggedIn, logout } = useStore()
const [showAuthModal, setShowAuthModal] = useState(false)
const [mounted, setMounted] = useState(false)
const [copied, setCopied] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
const copyCode = () => {
if (user?.referralCode) {
navigator.clipboard.writeText(user.referralCode)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
}
if (!mounted) {
return (
<div className="min-h-screen bg-black flex items-center justify-center">
<div className="animate-spin rounded-full h-6 w-6 border-t-2 border-b-2 border-[var(--app-brand)]" />
<div className="min-h-screen bg-app-bg flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-app-brand" />
</div>
)
}
if (!isLoggedIn) {
return (
<main className="min-h-screen bg-black text-white pb-20 flex flex-col items-center justify-center px-4">
<div className="w-12 h-12 rounded-full bg-white/5 flex items-center justify-center mb-4">
<User className="w-6 h-6 text-white/40" />
<main className="min-h-screen bg-app-bg text-app-text pb-20 flex flex-col items-center justify-center">
<div className="p-4 w-full">
<div className="max-w-xs mx-auto text-center">
<div className="w-14 h-14 mx-auto mb-3 rounded-full bg-app-card flex items-center justify-center">
<User className="w-7 h-7 text-app-text-muted" />
</div>
<h2 className="text-base font-semibold mb-1"></h2>
<p className="text-app-text-muted text-xs mb-4"></p>
<button
onClick={() => setShowAuthModal(true)}
className="bg-app-brand hover:bg-app-brand-hover text-white px-6 py-2.5 rounded-full font-medium text-sm"
>
</button>
</div>
</div>
<h2 className="text-base font-medium mb-1"></h2>
<p className="text-white/40 text-xs mb-6"></p>
<button
onClick={() => setShowAuthModal(true)}
className="bg-[var(--app-brand)] text-white px-8 py-2.5 rounded-full font-medium text-sm"
>
</button>
<AuthModal isOpen={showAuthModal} onClose={() => setShowAuthModal(false)} />
</main>
)
}
const totalSections = getAllSections().length
const purchasedCount = user?.hasFullBook ? totalSections : user?.purchasedSections.length || 0
const purchaseProgress = Math.round((purchasedCount / totalSections) * 100)
const fullBookPrice = getFullBookPrice()
return (
<main className="min-h-screen bg-black text-white pb-20">
{/* 用户信息头部 */}
<div className="px-4 pt-10 pb-6">
<div className="flex items-center gap-3 mb-6">
<div className="w-12 h-12 rounded-full bg-[var(--app-brand)]/20 flex items-center justify-center">
<User className="w-6 h-6 text-[var(--app-brand)]" />
<main className="min-h-screen bg-app-bg text-app-text pb-20">
{/* User Profile Header */}
<div className="bg-gradient-to-b from-app-card to-app-bg p-4 pt-8">
<div className="max-w-xs mx-auto">
<div className="flex items-center gap-3 mb-3">
<div className="w-11 h-11 rounded-full bg-app-brand/20 flex items-center justify-center">
<User className="w-5 h-5 text-app-brand" />
</div>
<div>
<h2 className="text-sm font-semibold">{user?.nickname || "用户"}</h2>
<p className="text-app-text-muted text-xs">{user?.phone}</p>
</div>
</div>
<div>
<h2 className="text-base font-medium">{user?.nickname || "用户"}</h2>
<p className="text-white/40 text-xs">{user?.phone}</p>
{/* Stats */}
<div className="grid grid-cols-2 gap-2">
<div className="bg-app-card/60 rounded-lg p-2.5 text-center">
<p className="text-base font-bold text-app-brand">
{user?.hasFullBook ? "全部" : user?.purchasedSections.length || 0}
</p>
<p className="text-app-text-muted text-xs"></p>
</div>
<div className="bg-app-card/60 rounded-lg p-2.5 text-center">
<p className="text-base font-bold text-app-brand">¥{(user?.earnings || 0).toFixed(1)}</p>
<p className="text-app-text-muted text-xs"></p>
</div>
</div>
</div>
{/* 数据统计卡片 */}
<div className="grid grid-cols-2 gap-3 mb-4">
{/* 购买进度 */}
<div className="bg-white/[0.03] rounded-2xl p-4 border border-white/[0.06]">
<div className="flex items-center justify-between mb-2">
<span className="text-white/40 text-xs"></span>
<span className="text-[var(--app-brand)] text-xs font-medium">{purchaseProgress}%</span>
</div>
<div className="h-1.5 bg-white/10 rounded-full overflow-hidden mb-2">
<div
className="h-full bg-[var(--app-brand)] rounded-full transition-all"
style={{ width: `${purchaseProgress}%` }}
/>
</div>
<p className="text-white text-lg font-bold">
{user?.hasFullBook ? "全书" : `${purchasedCount}/${totalSections}`}
</p>
<p className="text-white/30 text-[10px]"></p>
</div>
{/* 分销收益 */}
<div className="bg-white/[0.03] rounded-2xl p-4 border border-white/[0.06]">
<div className="flex items-center justify-between mb-2">
<span className="text-white/40 text-xs"></span>
<Share2 className="w-3.5 h-3.5 text-white/30" />
</div>
<p className="text-[var(--app-brand)] text-lg font-bold mb-1">¥{(user?.earnings || 0).toFixed(2)}</p>
<p className="text-white/30 text-[10px]">{user?.referralCount || 0}</p>
</div>
</div>
{/* 购买整本书提示 */}
{!user?.hasFullBook && (
<Link href="/chapters" className="block mb-4">
<div className="bg-gradient-to-r from-[var(--app-brand)]/10 to-purple-500/10 rounded-2xl p-4 border border-[var(--app-brand)]/20">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<BookOpen className="w-5 h-5 text-[var(--app-brand)]" />
<div>
<p className="text-white text-sm font-medium"></p>
<p className="text-white/40 text-xs">{totalSections}</p>
</div>
</div>
<span className="text-[var(--app-brand)] font-bold">¥{fullBookPrice}</span>
</div>
</div>
</Link>
)}
</div>
{/* 菜单列表 */}
<div className="px-4">
<div className="bg-white/[0.03] rounded-2xl overflow-hidden border border-white/[0.06]">
<Link href="/my/purchases" className="flex items-center justify-between p-4 border-b border-white/[0.04]">
<div className="flex items-center gap-3">
<ShoppingBag className="w-4 h-4 text-white/40" />
<span className="text-sm"></span>
</div>
<div className="flex items-center gap-2">
<span className="text-white/40 text-xs">{purchasedCount}</span>
<ChevronRight className="w-4 h-4 text-white/20" />
</div>
</Link>
{/* Menu Items */}
<div className="p-4">
<div className="max-w-xs mx-auto space-y-2">
{/* Purchase prompt */}
{!user?.hasFullBook && (
<Link href="/chapters" className="block">
<div className="bg-gradient-to-r from-app-brand/20 to-app-card rounded-lg p-3 border border-app-brand/30">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<BookOpen className="w-4 h-4 text-app-brand" />
<span className="text-app-text text-sm"></span>
</div>
<span className="text-app-brand font-bold text-sm">¥{fullBookPrice}</span>
</div>
</div>
</Link>
)}
<Link href="/my/referral" className="flex items-center justify-between p-4">
<div className="flex items-center gap-3">
<Share2 className="w-4 h-4 text-white/40" />
<span className="text-sm"></span>
</div>
<div className="flex items-center gap-2">
<span className="text-[var(--app-brand)] text-xs">¥{(user?.earnings || 0).toFixed(1)}</span>
<ChevronRight className="w-4 h-4 text-white/20" />
</div>
</Link>
</div>
{/* Menu List - simplified, removed settings and docs */}
<div className="bg-app-card/60 rounded-lg overflow-hidden">
<Link href="/my/purchases" className="flex items-center justify-between p-3 border-b border-app-border">
<div className="flex items-center gap-2">
<ShoppingBag className="w-4 h-4 text-app-text-muted" />
<span className="text-sm"></span>
</div>
<ChevronRight className="w-4 h-4 text-app-text-muted" />
</Link>
{/* 邀请码 */}
<div className="bg-white/[0.03] rounded-2xl p-4 border border-white/[0.06] mt-3">
<div className="flex items-center justify-between">
<div>
<p className="text-white/40 text-xs mb-1"></p>
<code className="text-[var(--app-brand)] font-mono text-sm">{user?.referralCode}</code>
</div>
<button
onClick={copyCode}
className="flex items-center gap-1 px-3 py-1.5 rounded-lg bg-white/5 text-white/60 text-xs"
>
{copied ? <Check className="w-3 h-3" /> : <Copy className="w-3 h-3" />}
{copied ? "已复制" : "复制"}
</button>
<Link href="/my/referral" className="flex items-center justify-between p-3">
<div className="flex items-center gap-2">
<Share2 className="w-4 h-4 text-app-text-muted" />
<span className="text-sm"></span>
</div>
<div className="flex items-center gap-1">
<span className="text-app-brand text-xs">¥{(user?.earnings || 0).toFixed(1)}</span>
<ChevronRight className="w-4 h-4 text-app-text-muted" />
</div>
</Link>
</div>
<p className="text-white/30 text-[10px] mt-2">90%</p>
</div>
{/* 退出登录 */}
<button
onClick={logout}
className="w-full flex items-center justify-center gap-2 p-3 mt-6 text-white/40 text-sm"
>
<LogOut className="w-4 h-4" />
<span>退</span>
</button>
{/* Referral Code */}
<div className="bg-app-card/60 rounded-lg p-3">
<div className="flex items-center justify-between">
<div>
<p className="text-app-text-muted text-xs"></p>
<code className="text-app-brand font-mono text-sm">{user?.referralCode}</code>
</div>
<button
onClick={() => navigator.clipboard.writeText(user?.referralCode || "")}
className="text-app-text-muted text-xs hover:text-app-text px-2 py-1 rounded bg-app-card"
>
</button>
</div>
</div>
{/* Logout */}
<button
onClick={logout}
className="w-full flex items-center justify-center gap-2 p-2.5 text-app-text-muted hover:text-red-400 transition-colors text-sm"
>
<LogOut className="w-4 h-4" />
<span>退</span>
</button>
</div>
</div>
</main>
)

View File

@@ -1,133 +1,9 @@
"use client"
import { HomeScreen } from "@/components/home-screen"
import { getBookStructure } from "@/lib/book-file-system"
import { useState, useEffect } from "react"
import Link from "next/link"
import { ChevronRight, Sparkles, User } from "lucide-react"
import { bookData, getAllSections, getFullBookPrice } from "@/lib/book-data"
import { useStore } from "@/lib/store"
import { AuthModal } from "@/components/modules/auth/auth-modal"
export const dynamic = "force-dynamic"
export default function HomePage() {
const [sectionsCount, setSectionsCount] = useState(64)
const [isAuthOpen, setIsAuthOpen] = useState(false)
const { isLoggedIn, user } = useStore()
useEffect(() => {
const sections = getAllSections()
setSectionsCount(sections.length)
}, [])
// 计算每篇的章节数
const partsWithCount = bookData.map((part) => ({
...part,
chaptersCount: part.chapters.length,
sectionsCount: part.chapters.reduce((acc, c) => acc + c.sections.length, 0),
}))
return (
<main className="min-h-screen bg-black text-white pb-20 overflow-hidden">
{/* 背景 */}
<div className="fixed inset-0 bg-gradient-to-b from-black via-[#0a0a0a] to-[#080808] -z-10" />
<div className="fixed top-0 left-1/2 -translate-x-1/2 w-[400px] h-[400px] bg-[var(--app-brand)] opacity-[0.04] blur-[100px] rounded-full -z-10" />
{/* 顶部用户状态栏 */}
<div className="px-4 pt-10 pb-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Sparkles className="w-4 h-4 text-[var(--app-brand)]" />
<span className="text-[var(--app-brand)] text-xs font-medium">Soul · </span>
</div>
{isLoggedIn ? (
<Link href="/my" className="flex items-center gap-1.5 text-xs text-white/60">
<User className="w-3.5 h-3.5" />
<span>{user?.nickname}</span>
</Link>
) : (
<button onClick={() => setIsAuthOpen(true)} className="text-xs text-[var(--app-brand)]">
</button>
)}
</div>
</div>
{/* 书籍信息头部 */}
<div className="px-4 pb-6 text-center">
<h1 className="text-2xl font-bold mb-2 leading-tight">
SOUL的
<span className="text-[var(--app-brand)]"></span>
</h1>
<p className="text-white/50 text-xs mb-3">Soul派对房的真实商业故事</p>
{/* 统计数据 */}
<div className="flex items-center justify-center gap-6 text-xs">
<div>
<span className="text-[var(--app-brand)] font-bold text-lg">{sectionsCount}</span>
<span className="text-white/40 ml-1"></span>
</div>
<div className="w-px h-4 bg-white/10" />
<div>
<span className="text-white font-bold text-lg">5</span>
<span className="text-white/40 ml-1"></span>
</div>
<div className="w-px h-4 bg-white/10" />
<div>
<span className="text-white/60"></span>
<span className="text-[var(--app-brand)] ml-1 font-medium"></span>
</div>
</div>
</div>
{/* 目录列表 - 紧凑显示在首页 */}
<div className="px-4">
<div className="bg-white/[0.03] rounded-2xl overflow-hidden border border-white/[0.06]">
{partsWithCount.map((part, index) => (
<Link key={part.id} href={`/chapters?part=${part.id}`} className="block">
<div
className={`flex items-center gap-3 p-3.5 active:bg-white/5 transition-colors ${
index !== partsWithCount.length - 1 ? "border-b border-white/[0.04]" : ""
}`}
>
{/* 序号 */}
<div className="w-8 h-8 rounded-lg bg-[var(--app-brand)]/10 flex items-center justify-center flex-shrink-0">
<span className="text-[var(--app-brand)] font-bold text-xs">{part.number}</span>
</div>
{/* 内容 */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<h3 className="text-white font-medium text-sm">{part.title}</h3>
<span className="text-white/30 text-[10px]">{part.sectionsCount}</span>
</div>
<p className="text-white/40 text-xs truncate">{part.subtitle}</p>
</div>
<ChevronRight className="w-4 h-4 text-white/20 flex-shrink-0" />
</div>
</Link>
))}
</div>
{/* 序言和尾声 */}
<div className="grid grid-cols-2 gap-2 mt-3">
<Link href="/read/preface" className="block">
<div className="bg-white/[0.03] rounded-xl p-3 border border-white/[0.06] active:bg-white/5">
<p className="text-white/30 text-[10px] mb-1"></p>
<p className="text-white text-xs leading-relaxed line-clamp-2">6Soul开播?</p>
</div>
</Link>
<Link href="/read/epilogue" className="block">
<div className="bg-white/[0.03] rounded-xl p-3 border border-white/[0.06] active:bg-white/5">
<p className="text-white/30 text-[10px] mb-1"></p>
<p className="text-white text-xs leading-relaxed line-clamp-2"></p>
</div>
</Link>
</div>
{/* 底部提示 */}
<p className="text-center text-white/30 text-[10px] mt-4 mb-2"> · ¥1 · ¥{getFullBookPrice()}</p>
</div>
<AuthModal isOpen={isAuthOpen} onClose={() => setIsAuthOpen(false)} />
</main>
)
export default async function HomePage() {
const parts = getBookStructure()
return <HomeScreen parts={parts} />
}

View File

@@ -2,56 +2,60 @@
import Link from "next/link"
import { usePathname } from "next/navigation"
import { Home, User, Handshake } from "lucide-react"
import { useState } from "react"
import { MatchModal } from "@/components/match-modal"
import { Home, User } from "lucide-react"
export function BottomNav() {
const pathname = usePathname()
const [showMatch, setShowMatch] = useState(false)
// 在管理后台不显示底部导航
if (pathname.startsWith("/admin")) {
// 在文档页面和管理后台不显示底部导航
if (pathname.startsWith("/documentation") || pathname.startsWith("/admin")) {
return null
}
const navItems = [
{ href: "/", icon: Home, label: "首页" },
{ href: "/match", emoji: "🤝", label: "匹配合作" },
{ href: "/my", icon: User, label: "我的" },
]
return (
<>
{/* iOS风格底部导航 - 只有3个按钮,匹配在当前页面弹窗 */}
<nav className="fixed bottom-0 left-0 right-0 z-40 bg-black/80 backdrop-blur-xl border-t border-white/[0.06] safe-bottom">
{/* iOS风格底部导航 - 只有3个按钮 */}
<nav className="fixed bottom-0 left-0 right-0 z-40 glass-nav safe-bottom">
<div className="flex items-center justify-around py-2 max-w-lg mx-auto">
{/* 首页 */}
<Link href="/" className="flex flex-col items-center py-2 px-6 transition-all">
<Home
className={`w-5 h-5 mb-0.5 ${pathname === "/" ? "text-[var(--app-brand)]" : "text-white/40"}`}
strokeWidth={pathname === "/" ? 2.5 : 1.5}
/>
<span className={`text-[10px] ${pathname === "/" ? "text-[var(--app-brand)]" : "text-white/40"}`}>
</span>
</Link>
{navItems.map((item, index) => {
const isActive = pathname === item.href || (item.href === '/match' && pathname.startsWith('/match'))
const Icon = item.icon
{/* 匹配合作 - 点击弹出弹窗而不是跳转 */}
<button onClick={() => setShowMatch(true)} className="flex flex-col items-center py-2 px-6 transition-all">
<Handshake className="w-5 h-5 mb-0.5 text-white/40" strokeWidth={1.5} />
<span className="text-[10px] text-white/40"></span>
</button>
{/* 我的 */}
<Link href="/my" className="flex flex-col items-center py-2 px-6 transition-all">
<User
className={`w-5 h-5 mb-0.5 ${pathname.startsWith("/my") ? "text-[var(--app-brand)]" : "text-white/40"}`}
strokeWidth={pathname.startsWith("/my") ? 2.5 : 1.5}
/>
<span className={`text-[10px] ${pathname.startsWith("/my") ? "text-[var(--app-brand)]" : "text-white/40"}`}>
</span>
</Link>
return (
<Link
key={index}
href={item.href!}
className="flex flex-col items-center py-2 px-4 sm:px-6 touch-feedback transition-all duration-200"
>
<div className={`w-7 h-7 flex items-center justify-center mb-1 transition-colors ${
isActive ? "text-[var(--app-brand)]" : "text-[var(--app-text-tertiary)]"
}`}>
{item.emoji ? (
<span className="text-2xl">{item.emoji}</span>
) : (
<Icon className="w-6 h-6" strokeWidth={isActive ? 2.5 : 1.5} />
)}
</div>
<span className={`text-[10px] font-medium transition-colors ${
isActive ? "text-[var(--app-brand)]" : "text-[var(--app-text-tertiary)]"
}`}>
{item.label}
</span>
{/* 激活指示器 */}
{isActive && (
<div className="absolute -bottom-0.5 w-1 h-1 rounded-full bg-[var(--app-brand)]" />
)}
</Link>
)
})}
</div>
</nav>
{/* 匹配弹窗 */}
<MatchModal isOpen={showMatch} onClose={() => setShowMatch(false)} />
</>
)
}

532
components/home-screen.tsx Normal file
View File

@@ -0,0 +1,532 @@
"use client"
import type React from "react"
import { useState, useEffect, useRef } from "react"
import { type Part, getAllSections, getFullBookPrice, specialSections } from "@/lib/book-data"
import { useStore } from "@/lib/store"
import { BookOpen, Lock, Check, Sparkles, ChevronRight, User, TrendingUp } from "lucide-react"
import { AuthModal } from "./modules/auth/auth-modal"
import { PaymentModal } from "./modules/payment/payment-modal"
import { ReadingModal } from "./reading-modal"
import { MatchingCircle } from "./matching-circle"
interface HomeScreenProps {
parts: Part[]
}
export function HomeScreen({ parts }: HomeScreenProps) {
const [activeTab, setActiveTab] = useState<"home" | "match" | "my">("home")
const [selectedSection, setSelectedSection] = useState<{ id: string; title: string; filePath: string } | null>(null)
const [isAuthOpen, setIsAuthOpen] = useState(false)
const [isPaymentOpen, setIsPaymentOpen] = useState(false)
const [paymentType, setPaymentType] = useState<"section" | "fullbook">("section")
const [paymentSectionId, setPaymentSectionId] = useState<string>("")
const [paymentSectionTitle, setPaymentSectionTitle] = useState<string>("")
const [paymentAmount, setPaymentAmount] = useState(1)
const { user, isLoggedIn, hasPurchased } = useStore()
const [mounted, setMounted] = useState(false)
const allSections = getAllSections()
const fullBookPrice = getFullBookPrice()
const totalSections = allSections.length
const purchasedCount = user?.hasFullBook ? totalSections : user?.purchasedSections?.length || 0
useEffect(() => {
setMounted(true)
}, [])
// 点击章节
const handleSectionClick = (section: {
id: string
title: string
filePath: string
isFree: boolean
price: number
}) => {
const canAccess = section.isFree || (isLoggedIn && hasPurchased(section.id))
if (canAccess) {
// 直接打开阅读弹窗
setSelectedSection({ id: section.id, title: section.title, filePath: section.filePath })
} else {
// 需要购买
if (!isLoggedIn) {
setIsAuthOpen(true)
} else {
setPaymentSectionId(section.id)
setPaymentSectionTitle(section.title)
setPaymentAmount(section.price)
setPaymentType("section")
setIsPaymentOpen(true)
}
}
}
// 购买全书
const handleBuyFullBook = () => {
if (!isLoggedIn) {
setIsAuthOpen(true)
return
}
setPaymentType("fullbook")
setPaymentAmount(fullBookPrice)
setIsPaymentOpen(true)
}
if (!mounted) {
return (
<div className="h-screen bg-black flex items-center justify-center">
<div className="w-8 h-8 border-2 border-[var(--app-brand)] border-t-transparent rounded-full animate-spin" />
</div>
)
}
return (
<div className="h-screen bg-black text-white flex flex-col overflow-hidden">
{/* 主内容区域 - 根据Tab切换 */}
<div className="flex-1 overflow-hidden">
{activeTab === "home" && (
<HomeTab
parts={parts}
totalSections={totalSections}
fullBookPrice={fullBookPrice}
purchasedCount={purchasedCount}
isLoggedIn={isLoggedIn}
hasPurchased={hasPurchased}
onSectionClick={handleSectionClick}
onBuyFullBook={handleBuyFullBook}
/>
)}
{activeTab === "match" && <MatchTab />}
{activeTab === "my" && (
<MyTab
user={user}
isLoggedIn={isLoggedIn}
totalSections={totalSections}
purchasedCount={purchasedCount}
onLogin={() => setIsAuthOpen(true)}
/>
)}
</div>
{/* 底部导航 - 固定三个Tab */}
<nav className="flex-shrink-0 glass-nav safe-bottom">
<div className="flex items-center justify-around py-2">
<TabButton
active={activeTab === "home"}
onClick={() => setActiveTab("home")}
icon={<BookOpen className="w-5 h-5" />}
label="首页"
/>
<TabButton
active={activeTab === "match"}
onClick={() => setActiveTab("match")}
icon={<Sparkles className="w-5 h-5" />}
label="匹配"
/>
<TabButton
active={activeTab === "my"}
onClick={() => setActiveTab("my")}
icon={<User className="w-5 h-5" />}
label="我的"
/>
</div>
</nav>
{/* 阅读弹窗 - 原地展示内容 */}
{selectedSection && (
<ReadingModal
section={selectedSection}
onClose={() => setSelectedSection(null)}
onPurchase={(sectionId, title, price) => {
setPaymentSectionId(sectionId)
setPaymentSectionTitle(title)
setPaymentAmount(price)
setPaymentType("section")
setIsPaymentOpen(true)
}}
/>
)}
{/* 弹窗 */}
<AuthModal isOpen={isAuthOpen} onClose={() => setIsAuthOpen(false)} />
<PaymentModal
isOpen={isPaymentOpen}
onClose={() => setIsPaymentOpen(false)}
type={paymentType}
sectionId={paymentSectionId}
sectionTitle={paymentSectionTitle}
amount={paymentAmount}
onSuccess={() => {
setIsPaymentOpen(false)
window.location.reload()
}}
/>
</div>
)
}
// Tab按钮组件
function TabButton({
active,
onClick,
icon,
label,
}: {
active: boolean
onClick: () => void
icon: React.ReactNode
label: string
}) {
return (
<button
onClick={onClick}
className={`flex flex-col items-center gap-1 px-6 py-2 transition-all touch-feedback ${
active ? "text-[var(--app-brand)]" : "text-[var(--app-text-tertiary)]"
}`}
>
{icon}
<span className="text-xs">{label}</span>
</button>
)
}
// 首页Tab - 书籍总览+完整目录
function HomeTab({
parts,
totalSections,
fullBookPrice,
purchasedCount,
isLoggedIn,
hasPurchased,
onSectionClick,
onBuyFullBook,
}: {
parts: Part[]
totalSections: number
fullBookPrice: number
purchasedCount: number
isLoggedIn: boolean
hasPurchased: (id: string) => boolean
onSectionClick: (section: any) => void
onBuyFullBook: () => void
}) {
const scrollRef = useRef<HTMLDivElement>(null)
return (
<div ref={scrollRef} className="h-full overflow-y-auto scrollbar-hide">
{/* 书籍总览区 - 精简版 */}
<div className="px-4 pt-8 pb-4">
<div className="text-center mb-6">
<div className="inline-flex items-center gap-2 px-3 py-1.5 glass-card mb-4">
<Sparkles className="w-3.5 h-3.5 text-[var(--app-brand)]" />
<span className="text-[var(--app-brand)] text-xs">Soul · </span>
</div>
<h1 className="text-2xl font-bold mb-2">SOUL的创业实验场</h1>
<p className="text-[var(--app-text-tertiary)] text-sm">Soul派对房的真实商业故事</p>
</div>
{/* 价格信息 */}
<div className="glass-card p-4 mb-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-6">
<div>
<p className="text-2xl font-bold text-[var(--app-brand)]">¥{fullBookPrice}</p>
<p className="text-[var(--app-text-tertiary)] text-xs"></p>
</div>
<div className="w-px h-10 bg-[var(--app-separator)]" />
<div>
<p className="text-2xl font-bold">{totalSections}</p>
<p className="text-[var(--app-text-tertiary)] text-xs"></p>
</div>
</div>
<button onClick={onBuyFullBook} className="btn-ios text-sm px-4 py-2">
</button>
</div>
</div>
</div>
{/* 完整目录 - 一次性展示所有章节 */}
<div className="px-4 pb-24">
<div className="flex items-center justify-between mb-4">
<h2 className="text-[var(--app-text-secondary)] text-sm font-medium"></h2>
<span className="text-[var(--app-text-tertiary)] text-xs">
{purchasedCount}/{totalSections}
</span>
</div>
{/* 序言 */}
<SectionItem
id="preface"
number="序"
title="为什么我每天早上6点在Soul开播?"
isFree={true}
isPurchased={true}
onClick={() =>
onSectionClick({
id: "preface",
title: specialSections.preface.title,
filePath: specialSections.preface.filePath,
isFree: true,
price: 0,
})
}
/>
{/* 所有篇章和小节 */}
{parts.map((part) => (
<div key={part.id} className="mb-4">
{/* 篇章标题 */}
<div className="flex items-center gap-3 py-3 px-2">
<div className="w-8 h-8 rounded-lg bg-[var(--app-brand-light)] flex items-center justify-center">
<span className="text-[var(--app-brand)] font-bold text-sm">{part.number}</span>
</div>
<div>
<h3 className="text-white font-semibold text-sm">{part.title}</h3>
<p className="text-[var(--app-text-tertiary)] text-xs">{part.subtitle}</p>
</div>
</div>
{/* 该篇章下的所有小节 */}
<div className="glass-card overflow-hidden">
{part.chapters.map((chapter) =>
chapter.sections.map((section, sectionIndex) => {
const isPurchased = isLoggedIn && hasPurchased(section.id)
return (
<SectionItem
key={section.id}
id={section.id}
number={section.id}
title={section.title}
isFree={section.isFree}
isPurchased={isPurchased}
price={section.price}
isLast={sectionIndex === chapter.sections.length - 1}
onClick={() => onSectionClick(section)}
/>
)
}),
)}
</div>
</div>
))}
{/* 尾声 */}
<SectionItem
id="epilogue"
number="尾"
title="努力不是关键,选择才是"
isFree={true}
isPurchased={true}
onClick={() =>
onSectionClick({
id: "epilogue",
title: specialSections.epilogue.title,
filePath: specialSections.epilogue.filePath,
isFree: true,
price: 0,
})
}
/>
</div>
</div>
)
}
// 章节列表项
function SectionItem({
id,
number,
title,
isFree,
isPurchased,
price = 1,
isLast = false,
onClick,
}: {
id: string
number: string
title: string
isFree: boolean
isPurchased: boolean
price?: number
isLast?: boolean
onClick: () => void
}) {
const canAccess = isFree || isPurchased
return (
<button
onClick={onClick}
className={`w-full flex items-center gap-3 px-4 py-3 text-left transition-all touch-feedback ${
!isLast ? "border-b border-[var(--app-separator)]" : ""
}`}
>
{/* 状态图标 */}
<div
className={`w-6 h-6 rounded-full flex items-center justify-center flex-shrink-0 ${
canAccess ? "bg-[var(--app-brand-light)]" : "bg-[var(--app-bg-tertiary)]"
}`}
>
{canAccess ? (
<Check className="w-3.5 h-3.5 text-[var(--app-brand)]" />
) : (
<Lock className="w-3 h-3 text-[var(--app-text-tertiary)]" />
)}
</div>
{/* 编号和标题 */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-[var(--app-brand)] text-xs font-medium">{number}</span>
<span className={`text-sm truncate ${canAccess ? "text-white" : "text-[var(--app-text-secondary)]"}`}>
{title}
</span>
</div>
</div>
{/* 价格/状态 */}
<div className="flex-shrink-0">
{isFree ? (
<span className="text-[var(--app-brand)] text-xs"></span>
) : isPurchased ? (
<span className="text-[var(--app-text-tertiary)] text-xs"></span>
) : (
<span className="text-[var(--ios-orange)] text-xs">¥{price}</span>
)}
</div>
<ChevronRight className="w-4 h-4 text-[var(--app-text-tertiary)] flex-shrink-0" />
</button>
)
}
// 匹配Tab - 圆形UI高级感
function MatchTab() {
return (
<div className="h-full flex flex-col items-center justify-center px-4">
<MatchingCircle />
</div>
)
}
// 我的Tab - 数据中心
function MyTab({
user,
isLoggedIn,
totalSections,
purchasedCount,
onLogin,
}: {
user: any
isLoggedIn: boolean
totalSections: number
purchasedCount: number
onLogin: () => void
}) {
if (!isLoggedIn) {
return (
<div className="h-full flex flex-col items-center justify-center px-4">
<div className="w-20 h-20 rounded-full bg-[var(--app-bg-secondary)] flex items-center justify-center mb-4">
<User className="w-10 h-10 text-[var(--app-text-tertiary)]" />
</div>
<h2 className="text-lg font-semibold mb-2"></h2>
<p className="text-[var(--app-text-tertiary)] text-sm mb-6 text-center"></p>
<button onClick={onLogin} className="btn-ios px-8">
</button>
</div>
)
}
const readingProgress = user?.hasFullBook ? 100 : Math.round((purchasedCount / totalSections) * 100)
const earnings = user?.earnings || 0
return (
<div className="h-full overflow-y-auto scrollbar-hide px-4 pt-8 pb-24">
{/* 用户信息 */}
<div className="flex items-center gap-4 mb-8">
<div className="w-16 h-16 rounded-full bg-[var(--app-brand-light)] flex items-center justify-center">
<User className="w-8 h-8 text-[var(--app-brand)]" />
</div>
<div>
<h2 className="text-xl font-semibold">{user?.nickname || "用户"}</h2>
<p className="text-[var(--app-text-tertiary)] text-sm">{user?.phone}</p>
</div>
</div>
{/* 数据卡片 - 清晰可视化 */}
<div className="grid grid-cols-2 gap-3 mb-6">
{/* 已购章节 */}
<div className="glass-card p-4">
<div className="flex items-center gap-2 mb-2">
<BookOpen className="w-4 h-4 text-[var(--ios-blue)]" />
<span className="text-[var(--app-text-tertiary)] text-xs"></span>
</div>
<p className="text-2xl font-bold text-white">{user?.hasFullBook ? "全部" : purchasedCount}</p>
<p className="text-[var(--app-text-tertiary)] text-xs"> {totalSections} </p>
</div>
{/* 累计收益 */}
<div className="glass-card p-4">
<div className="flex items-center gap-2 mb-2">
<TrendingUp className="w-4 h-4 text-[var(--app-brand)]" />
<span className="text-[var(--app-text-tertiary)] text-xs"></span>
</div>
<p className="text-2xl font-bold text-[var(--app-brand)]">¥{earnings.toFixed(1)}</p>
<p className="text-[var(--app-text-tertiary)] text-xs"></p>
</div>
</div>
{/* 阅读进度 */}
<div className="glass-card p-4 mb-6">
<div className="flex items-center justify-between mb-3">
<span className="text-[var(--app-text-secondary)] text-sm"></span>
<span className="text-[var(--app-brand)] font-semibold">{readingProgress}%</span>
</div>
<div className="progress-bar">
<div className="progress-bar-fill" style={{ width: `${readingProgress}%` }} />
</div>
<p className="text-[var(--app-text-tertiary)] text-xs mt-2">
{user?.hasFullBook ? "已拥有全书" : `还差 ${totalSections - purchasedCount} 章解锁全部内容`}
</p>
</div>
{/* 邀请码 */}
<div className="glass-card p-4 mb-6">
<div className="flex items-center justify-between">
<div>
<p className="text-[var(--app-text-tertiary)] text-xs mb-1"></p>
<code className="text-[var(--app-brand)] font-mono text-lg">{user?.referralCode}</code>
</div>
<button
onClick={() => {
navigator.clipboard.writeText(user?.referralCode || "")
alert("邀请码已复制!")
}}
className="btn-ios-secondary text-sm px-4 py-2"
>
</button>
</div>
<p className="text-[var(--app-text-tertiary)] text-xs mt-3"> 90% </p>
</div>
{/* 退出登录 */}
<button
onClick={() => {
useStore.getState().logout()
window.location.reload()
}}
className="w-full text-center py-3 text-red-400 text-sm"
>
退
</button>
</div>
)
}

View File

@@ -1,215 +0,0 @@
"use client"
import { useState, useEffect } from "react"
import { X, Copy, Check, RefreshCw } from "lucide-react"
import { motion, AnimatePresence } from "framer-motion"
interface MatchUser {
id: string
nickname: string
tags: string[]
matchScore: number
concept: string
wechat: string
}
interface MatchModalProps {
isOpen: boolean
onClose: () => void
}
export function MatchModal({ isOpen, onClose }: MatchModalProps) {
const [isMatching, setIsMatching] = useState(false)
const [currentMatch, setCurrentMatch] = useState<MatchUser | null>(null)
const [copied, setCopied] = useState(false)
const getMockMatch = (): MatchUser => {
const data = [
{ nickname: "创业小白", tags: ["电商", "私域"], concept: "想找供应链合作伙伴", wechat: "soul_biz_001" },
{ nickname: "私域达人", tags: ["内容", "直播"], concept: "擅长内容运营,找技术合伙人", wechat: "soul_biz_002" },
{ nickname: "供应链老兵", tags: ["供应链", "跨境"], concept: "有工厂资源,找流量渠道", wechat: "soul_biz_003" },
{ nickname: "技术宅", tags: ["AI", "开发"], concept: "会写代码,想找商业方向", wechat: "soul_biz_004" },
]
const item = data[Math.floor(Math.random() * data.length)]
return {
id: `user_${Date.now()}`,
...item,
matchScore: Math.floor(Math.random() * 20) + 80,
}
}
const startMatch = () => {
setIsMatching(true)
setCurrentMatch(null)
setCopied(false)
setTimeout(() => {
setIsMatching(false)
setCurrentMatch(getMockMatch())
}, 2000)
}
const copyWechat = () => {
if (currentMatch) {
navigator.clipboard.writeText(currentMatch.wechat)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
}
useEffect(() => {
if (!isOpen) {
setCurrentMatch(null)
setIsMatching(false)
}
}, [isOpen])
if (!isOpen) return null
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* 背景遮罩 */}
<div className="absolute inset-0 bg-black/80 backdrop-blur-sm" onClick={onClose} />
{/* 弹窗内容 */}
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="relative w-full max-w-sm bg-[#111] rounded-3xl overflow-hidden border border-white/10"
>
{/* 关闭按钮 */}
<button
onClick={onClose}
className="absolute top-4 right-4 w-8 h-8 rounded-full bg-white/10 flex items-center justify-center z-10"
>
<X className="w-4 h-4 text-white/60" />
</button>
<div className="p-6 pt-12">
<AnimatePresence mode="wait">
{/* 初始状态 - 开始匹配 */}
{!isMatching && !currentMatch && (
<motion.div
key="idle"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="text-center"
>
<h2 className="text-xl font-bold text-white mb-2"></h2>
<p className="text-white/50 text-sm mb-8"></p>
{/* 匹配按钮 - 美化的圆形按钮 */}
<button onClick={startMatch} className="relative w-40 h-40 mx-auto mb-8 group">
{/* 外圈光环 */}
<div className="absolute inset-0 rounded-full bg-gradient-to-br from-[var(--app-brand)]/20 to-purple-500/20 animate-pulse" />
<div className="absolute inset-2 rounded-full bg-gradient-to-br from-[var(--app-brand)]/30 to-purple-500/30" />
{/* 主按钮 */}
<div className="absolute inset-4 rounded-full bg-gradient-to-br from-[var(--app-brand)] to-purple-500 flex flex-col items-center justify-center shadow-lg shadow-[var(--app-brand)]/30 group-active:scale-95 transition-transform">
<span className="text-4xl mb-1">🤝</span>
<span className="text-white font-medium text-sm"></span>
</div>
</button>
<div className="space-y-2 text-left">
<div className="flex items-center gap-2 text-white/50 text-xs">
<span>💼</span>
<span></span>
</div>
<div className="flex items-center gap-2 text-white/50 text-xs">
<span>🔒</span>
<span></span>
</div>
</div>
</motion.div>
)}
{/* 匹配中 */}
{isMatching && (
<motion.div
key="matching"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="text-center py-12"
>
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 2, repeat: Number.POSITIVE_INFINITY, ease: "linear" }}
className="w-20 h-20 mx-auto mb-6 rounded-full border-4 border-[var(--app-brand)]/30 border-t-[var(--app-brand)]"
/>
<p className="text-white font-medium">...</p>
<p className="text-white/40 text-sm mt-1"></p>
</motion.div>
)}
{/* 匹配成功 */}
{currentMatch && !isMatching && (
<motion.div
key="matched"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
>
<div className="text-center mb-4">
<span className="text-3xl"></span>
<h3 className="text-lg font-bold text-white mt-2">!</h3>
</div>
{/* 用户卡片 */}
<div className="bg-white/5 rounded-2xl p-4 mb-4">
<div className="flex items-center justify-between mb-3">
<div>
<h4 className="text-white font-medium">{currentMatch.nickname}</h4>
<div className="flex gap-1 mt-1">
{currentMatch.tags.map((tag) => (
<span
key={tag}
className="px-2 py-0.5 rounded-full text-[10px] bg-[var(--app-brand)]/20 text-[var(--app-brand)]"
>
{tag}
</span>
))}
</div>
</div>
<div className="text-right">
<span className="text-2xl font-bold text-[var(--app-brand)]">{currentMatch.matchScore}%</span>
<p className="text-white/40 text-[10px]"></p>
</div>
</div>
<p className="text-white/60 text-sm">{currentMatch.concept}</p>
</div>
{/* 微信号 */}
<div className="bg-white/5 rounded-xl p-3 mb-4">
<p className="text-white/40 text-xs mb-1"></p>
<div className="flex items-center justify-between">
<code className="text-[var(--app-brand)] font-mono">{currentMatch.wechat}</code>
<button
onClick={copyWechat}
className="flex items-center gap-1 px-3 py-1.5 rounded-lg bg-[var(--app-brand)] text-white text-xs"
>
{copied ? <Check className="w-3 h-3" /> : <Copy className="w-3 h-3" />}
{copied ? "已复制" : "复制"}
</button>
</div>
</div>
{/* 重新匹配 */}
<button
onClick={startMatch}
className="w-full py-3 rounded-xl bg-white/5 text-white/60 text-sm flex items-center justify-center gap-2"
>
<RefreshCw className="w-4 h-4" />
</button>
</motion.div>
)}
</AnimatePresence>
</div>
</motion.div>
</div>
)
}

View File

@@ -0,0 +1,171 @@
"use client"
import { useState, useEffect } from "react"
import { Sparkles, Users, BookOpen } from "lucide-react"
import { getAllSections } from "@/lib/book-data"
import { useStore } from "@/lib/store"
export function MatchingCircle() {
const [isMatching, setIsMatching] = useState(false)
const [matchProgress, setMatchProgress] = useState(0)
const [matchResult, setMatchResult] = useState<{
section: { id: string; title: string }
reason: string
compatibility: number
} | null>(null)
const { user, isLoggedIn } = useStore()
const allSections = getAllSections()
// 开始匹配
const startMatching = () => {
if (isMatching) return
setIsMatching(true)
setMatchProgress(0)
setMatchResult(null)
// 模拟匹配进度
const interval = setInterval(() => {
setMatchProgress((prev) => {
if (prev >= 100) {
clearInterval(interval)
// 匹配完成,生成结果
const randomSection = allSections[Math.floor(Math.random() * allSections.length)]
const reasons = [
"与你的创业方向高度匹配",
"适合你当前的发展阶段",
"契合你的商业思维模式",
"与你的行业背景相关",
"符合你的学习偏好",
]
setMatchResult({
section: { id: randomSection.id, title: randomSection.title },
reason: reasons[Math.floor(Math.random() * reasons.length)],
compatibility: Math.floor(Math.random() * 20) + 80,
})
setIsMatching(false)
return 100
}
return prev + 2
})
}, 50)
}
// 保存匹配结果到本地
useEffect(() => {
if (matchResult && isLoggedIn) {
const savedResults = JSON.parse(localStorage.getItem("match_results") || "[]")
savedResults.unshift({
...matchResult,
userId: user?.id,
matchedAt: new Date().toISOString(),
})
// 只保留最近10条
localStorage.setItem("match_results", JSON.stringify(savedResults.slice(0, 10)))
}
}, [matchResult, isLoggedIn, user?.id])
return (
<div className="w-full max-w-sm text-center">
{/* 匹配圆环 */}
<div className="relative w-64 h-64 mx-auto mb-8">
{/* 外圈装饰 */}
<div className="absolute inset-0 rounded-full border-2 border-[var(--app-border)] opacity-30" />
<div className="absolute inset-2 rounded-full border border-[var(--app-border)] opacity-20" />
<div className="absolute inset-4 rounded-full border border-[var(--app-border)] opacity-10" />
{/* 进度圆环 */}
<svg className="absolute inset-0 w-full h-full -rotate-90">
<circle cx="128" cy="128" r="120" fill="none" stroke="var(--app-bg-tertiary)" strokeWidth="4" />
<circle
cx="128"
cy="128"
r="120"
fill="none"
stroke="var(--app-brand)"
strokeWidth="4"
strokeLinecap="round"
strokeDasharray={`${2 * Math.PI * 120}`}
strokeDashoffset={`${2 * Math.PI * 120 * (1 - matchProgress / 100)}`}
className="transition-all duration-100"
style={{
filter: isMatching ? "drop-shadow(0 0 10px var(--app-brand))" : "none",
}}
/>
</svg>
{/* 中心内容 */}
<div className="absolute inset-8 rounded-full glass-card flex flex-col items-center justify-center">
{isMatching ? (
<>
<Sparkles className="w-10 h-10 text-[var(--app-brand)] animate-pulse mb-2" />
<p className="text-[var(--app-brand)] text-2xl font-bold">{matchProgress}%</p>
<p className="text-[var(--app-text-tertiary)] text-xs">...</p>
</>
) : matchResult ? (
<>
<div className="text-[var(--app-brand)] text-3xl font-bold mb-1">{matchResult.compatibility}%</div>
<p className="text-white text-xs mb-1"></p>
<p className="text-[var(--app-text-tertiary)] text-[10px]">{matchResult.reason}</p>
</>
) : (
<>
<Users className="w-10 h-10 text-[var(--app-text-tertiary)] mb-2" />
<p className="text-white text-sm"></p>
<p className="text-[var(--app-text-tertiary)] text-xs"></p>
</>
)}
</div>
{/* 浮动装饰点 */}
{isMatching && (
<>
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-2 h-2 rounded-full bg-[var(--app-brand)] animate-ping" />
<div
className="absolute bottom-0 left-1/2 -translate-x-1/2 w-2 h-2 rounded-full bg-[var(--ios-blue)] animate-ping"
style={{ animationDelay: "0.5s" }}
/>
<div
className="absolute left-0 top-1/2 -translate-y-1/2 w-2 h-2 rounded-full bg-[var(--ios-purple)] animate-ping"
style={{ animationDelay: "0.25s" }}
/>
<div
className="absolute right-0 top-1/2 -translate-y-1/2 w-2 h-2 rounded-full bg-[var(--ios-teal)] animate-ping"
style={{ animationDelay: "0.75s" }}
/>
</>
)}
</div>
{/* 匹配结果 */}
{matchResult && (
<div className="glass-card p-4 mb-6 text-left">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-[var(--app-brand-light)] flex items-center justify-center">
<BookOpen className="w-5 h-5 text-[var(--app-brand)]" />
</div>
<div className="flex-1 min-w-0">
<p className="text-[var(--app-brand)] text-xs mb-0.5">{matchResult.section.id}</p>
<p className="text-white text-sm truncate">{matchResult.section.title}</p>
</div>
</div>
</div>
)}
{/* 匹配按钮 */}
<button
onClick={startMatching}
disabled={isMatching}
className={`btn-ios w-full flex items-center justify-center gap-2 ${
isMatching ? "opacity-50 cursor-not-allowed" : "glow"
}`}
>
<Sparkles className="w-5 h-5" />
<span>{isMatching ? "匹配中..." : matchResult ? "重新匹配" : "开始匹配"}</span>
</button>
<p className="text-[var(--app-text-tertiary)] text-xs mt-4"></p>
</div>
)
}

View File

@@ -0,0 +1,125 @@
"use client"
import { useState, useEffect } from "react"
import { X, Lock, Sparkles } from "lucide-react"
import { useStore } from "@/lib/store"
import { getFullBookPrice } from "@/lib/book-data"
interface ReadingModalProps {
section: { id: string; title: string; filePath: string }
onClose: () => void
onPurchase: (sectionId: string, title: string, price: number) => void
}
export function ReadingModal({ section, onClose, onPurchase }: ReadingModalProps) {
const [content, setContent] = useState("")
const [isLoading, setIsLoading] = useState(true)
const { isLoggedIn, hasPurchased } = useStore()
const isFree = section.id === "preface" || section.id === "epilogue"
const canAccess = isFree || (isLoggedIn && hasPurchased(section.id))
const fullBookPrice = getFullBookPrice()
useEffect(() => {
async function loadContent() {
try {
const response = await fetch(`/api/content?path=${encodeURIComponent(section.filePath)}`)
if (response.ok) {
const data = await response.json()
setContent(data.content || "")
}
} catch (error) {
console.error("Failed to load content:", error)
} finally {
setIsLoading(false)
}
}
loadContent()
}, [section.filePath])
// 计算显示内容
const displayContent = canAccess ? content : content.slice(0, Math.floor(content.length * 0.3))
const showPaywall = !canAccess && content.length > 0
return (
<div className="fixed inset-0 z-50 bg-black/90 modal-overlay">
<div className="h-full flex flex-col">
{/* Header */}
<header className="flex-shrink-0 glass-nav px-4 py-3 flex items-center justify-between safe-top">
<button
onClick={onClose}
className="w-9 h-9 rounded-full bg-[var(--app-bg-secondary)] flex items-center justify-center touch-feedback"
>
<X className="w-5 h-5" />
</button>
<h1 className="text-white font-semibold text-sm truncate flex-1 mx-4 text-center">{section.title}</h1>
<div className="w-9" />
</header>
{/* Content */}
<div className="flex-1 overflow-y-auto scrollbar-hide">
<div className="max-w-2xl mx-auto px-5 py-6">
{isLoading ? (
<div className="space-y-4">
{[...Array(10)].map((_, i) => (
<div key={i} className="skeleton h-4 rounded" style={{ width: `${Math.random() * 40 + 60}%` }} />
))}
</div>
) : (
<>
<article className="book-content relative">
<div className="text-[var(--app-text-secondary)] leading-[1.9] text-[17px]">
{displayContent.split("\n").map(
(paragraph, index) =>
paragraph.trim() && (
<p key={index} className="mb-6 text-justify">
{paragraph}
</p>
),
)}
</div>
{/* 付费墙渐变 */}
{showPaywall && (
<div className="absolute bottom-0 left-0 right-0 h-48 bg-gradient-to-t from-black to-transparent" />
)}
</article>
{/* 付费提示 - 在阅读中途触发 */}
{showPaywall && (
<div className="mt-8 glass-card-heavy p-8 text-center">
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-[var(--app-bg-secondary)] flex items-center justify-center">
<Lock className="w-8 h-8 text-[var(--app-text-tertiary)]" />
</div>
<h3 className="text-xl font-semibold mb-2"></h3>
<p className="text-[var(--app-text-secondary)] mb-6">30%</p>
<div className="flex flex-col gap-3">
<button
onClick={() => onPurchase(section.id, section.title, 1)}
className="btn-ios-secondary py-3"
>
¥1
</button>
<button
onClick={() => onPurchase(section.id, section.title, fullBookPrice)}
className="btn-ios py-3 glow flex items-center justify-center gap-2"
>
<Sparkles className="w-4 h-4" />
¥{fullBookPrice} 82%
</button>
</div>
<p className="text-[var(--app-text-tertiary)] text-xs mt-4">
90%
</p>
</div>
)}
</>
)}
</div>
</div>
</div>
</div>
)
}

View File

@@ -24,12 +24,13 @@ export interface Part {
}
export const BASE_BOOK_PRICE = 9.9
export const PRICE_INCREMENT_PER_SECTION = 1
export const SECTION_PRICE = 1
export const AUTHOR_SHARE = 0.9
export const DISTRIBUTOR_SHARE = 0.1
export const FULL_BOOK_PRICE = 9.9
export function getFullBookPrice(): number {
return FULL_BOOK_PRICE
export function getFullBookPrice(sectionsCount?: number): number {
return 9.9
}
export const bookData: Part[] = [
@@ -45,39 +46,40 @@ export const bookData: Part[] = [
sections: [
{
id: "1.1",
title: "荷包:电动车出租的被动收入模式",
title: "自行车荷总:一个行业做到极致是什么样",
price: 1,
isFree: true,
filePath: "book/第一篇|真实的人/第1章人与人之间的底层逻辑/1.1 荷包:电动车出租的被动收入模式.md",
filePath: "book/_第一篇|真实的人/第1章人与人之间的底层逻辑/1.1 自行车荷总:一个行业做到极致是什么样.md",
},
{
id: "1.2",
title: "老墨:资源整合高手的社交方法",
price: 1,
isFree: false,
filePath: "book/第一篇|真实的人/第1章人与人之间的底层逻辑/1.2 老墨:资源整合高手的社交方法.md",
unlockAfterDays: 3,
filePath: "book/_第一篇真实的人/第1章人与人之间的底层逻辑/1.2 老墨:资源整合高手的社交方法.md",
},
{
id: "1.3",
title: "笑声背后的MBTI为什么ENTJ适合做资源INTP适合做系统",
title: "笑声背后的MBTI:为什么ENTJ适合做资源,INTP适合做系统",
price: 1,
isFree: false,
filePath:
"book/第一篇|真实的人/第1章人与人之间的底层逻辑/1.3 笑声背后的MBTI为什么ENTJ适合做资源INTP适合做系统.md",
"book/_第一篇|真实的人/第1章人与人之间的底层逻辑/1.3 笑声背后的MBTI为什么ENTJ适合做资源INTP适合做系统.md",
},
{
id: "1.4",
title: "人性的三角结构:利益、情感、价值观",
title: "人性的三角结构:情绪、价值、利益",
price: 1,
isFree: false,
filePath: "book/第一篇|真实的人/第1章人与人之间的底层逻辑/1.4 人性的三角结构:利益、情感、价值观.md",
filePath: "book/_第一篇|真实的人/第1章人与人之间的底层逻辑/1.4 人性的三角结构:情绪、价值、利益.md",
},
{
id: "1.5",
title: "沟通差的问题:为什么你说的别人听不懂",
title: "为什么99%的合作死在沟通差而不是能力差",
price: 1,
isFree: false,
filePath: "book/第一篇|真实的人/第1章人与人之间的底层逻辑/1.5 沟通差的问题:为什么你说的别人听不懂.md",
filePath: "book/_第一篇|真实的人/第1章人与人之间的底层逻辑/1.5 为什么99%的合作死在沟通差而不是能力差.md",
},
],
},
@@ -90,36 +92,42 @@ export const bookData: Part[] = [
title: "相亲故事:你以为找的是人,实际是在找模式",
price: 1,
isFree: false,
filePath: "book/第一篇|真实的人/第2章人性困境案例/2.1 相亲故事:你以为找的是人,实际是在找模式.md",
unlockAfterDays: 3,
filePath: "book/_第一篇真实的人/第2章人性困境案例/2.1 相亲故事:你以为找的是人,实际是在找模式.md",
},
{
id: "2.2",
title: "找工作迷茫者:为什么简历解决不了人生",
price: 1,
isFree: false,
filePath: "book/第一篇|真实的人/第2章人性困境案例/2.2 找工作迷茫者:为什么简历解决不了人生.md",
unlockAfterDays: 3,
filePath: "book/_第一篇真实的人/第2章人性困境案例/2.2 找工作迷茫者:为什么简历解决不了人生.md",
},
{
id: "2.3",
title: "撸运费险:小钱困住大脑的真实心理",
price: 1,
isFree: false,
filePath: "book/第一篇|真实的人/第2章人性困境案例/2.3 撸运费险:小钱困住大脑的真实心理.md",
unlockAfterDays: 3,
filePath: "book/_第一篇真实的人/第2章人性困境案例/2.3 撸运费险:小钱困住大脑的真实心理.md",
},
{
id: "2.4",
title: "游戏上瘾的年轻人:不是游戏吸引他,是生活没吸引力",
price: 1,
isFree: false,
filePath: "book/第一篇|真实的人/第2章人性困境案例/2.4 游戏上瘾的年轻人:不是游戏吸引他,是生活没吸引力.md",
unlockAfterDays: 3,
filePath:
"book/_第一篇真实的人/第2章人性困境案例/2.4 游戏上瘾的年轻人:不是游戏吸引他,是生活没吸引力.md",
},
{
id: "2.5",
title: "健康焦虑(我的糖尿病经历):疾病是人生的第一次清醒",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath:
"book/第一篇|真实的人/第2章人性困境案例/2.5 健康焦虑(我的糖尿病经历):疾病是人生的第一次清醒.md",
"book/_第一篇|真实的人/第2章人性困境案例/2.5 健康焦虑(我的糖尿病经历):疾病是人生的第一次清醒.md",
},
],
},
@@ -137,31 +145,43 @@ export const bookData: Part[] = [
sections: [
{
id: "3.1",
title: "3000万流水如何跑出来(退税模式解析)",
title: "电商财税窗口:我错过的第一桶金",
price: 1,
isFree: false,
filePath: "book/第二篇|真实的行业/第3章电商篇/3.1 3000万流水如何跑出来(退税模式解析).md",
unlockAfterDays: 3,
filePath: "book/第二篇|真实的行业/第3章电商篇/3.1 电商财税窗口:我错过的第一桶金.md",
},
{
id: "3.2",
title: "供应链之王 vs 打工人:利润不在前端",
title: "3000万流水如何跑出来(退税模式解析)",
price: 1,
isFree: false,
filePath: "book/第二篇|真实的行业/第3章电商篇/3.2 供应链之王 vs 打工人:利润不在前端.md",
unlockAfterDays: 3,
filePath: "book/第二篇|真实的行业/第3章电商篇/3.2 3000万流水如何跑出来(退税模式解析).md",
},
{
id: "3.3",
title: "社区团购的底层逻辑",
title: "供应链之王vs打工人:利润不在前端",
price: 1,
isFree: false,
filePath: "book/第二篇|真实的行业/第3章电商篇/3.3 社区团购的底层逻辑.md",
unlockAfterDays: 3,
filePath: "book/第二篇|真实的行业/第3章电商篇/3.3 供应链之王 vs 打工人:利润不在前端.md",
},
{
id: "3.4",
title: "社区团购的底层逻辑",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第二篇|真实的行业/第3章电商篇/3.4 社区团购的底层逻辑.md",
},
{
id: "3.5",
title: "跨境电商与退税套利",
price: 1,
isFree: false,
filePath: "book/第二篇|真实的行业/第3章电商篇/3.4 跨境电商与退税套利.md",
unlockAfterDays: 3,
filePath: "book/第二篇|真实的行业/第3章电商篇/3.5 跨境电商与退税套利.md",
},
],
},
@@ -174,6 +194,7 @@ export const bookData: Part[] = [
title: "旅游号:30天10万粉的真实逻辑",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第二篇|真实的行业/第4章内容商业篇/4.1 旅游号:30天10万粉的真实逻辑.md",
},
{
@@ -181,6 +202,7 @@ export const bookData: Part[] = [
title: "做号工厂:如何让一个号变成一个机器",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第二篇|真实的行业/第4章内容商业篇/4.2 做号工厂:如何让一个号变成一个机器.md",
},
{
@@ -188,6 +210,7 @@ export const bookData: Part[] = [
title: "情绪内容为什么比专业内容更赚钱",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第二篇|真实的行业/第4章内容商业篇/4.3 情绪内容为什么比专业内容更赚钱.md",
},
{
@@ -195,6 +218,7 @@ export const bookData: Part[] = [
title: "猫与宠物号:为什么宠物赛道永不过时",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第二篇|真实的行业/第4章内容商业篇/4.4 猫与宠物号:为什么宠物赛道永不过时.md",
},
{
@@ -202,6 +226,7 @@ export const bookData: Part[] = [
title: "直播间里的三种人:演员、技术工、系统流",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第二篇|真实的行业/第4章内容商业篇/4.5 直播间里的三种人:演员、技术工、系统流.md",
},
],
@@ -212,38 +237,43 @@ export const bookData: Part[] = [
sections: [
{
id: "5.1",
title: "拍卖行抱朴一天240万的摇号生意",
title: "羽毛球馆:为什么体育培训是最稳定的现金流",
price: 1,
isFree: false,
filePath: "book/第二篇|真实的行业/第5章传统行业篇/5.1 拍卖行抱朴一天240万的摇号生意.md",
unlockAfterDays: 3,
filePath: "book/第二篇|真实的行业/第5章传统行业篇/5.1 羽毛球馆:为什么体育培训是最稳定的现金流.md",
},
{
id: "5.2",
title: "土地拍卖:招拍挂背后的游戏规则",
title: "旅游供应链:资源越老越值钱",
price: 1,
isFree: false,
filePath: "book/第二篇|真实的行业/第5章传统行业篇/5.2 土地拍卖:招拍挂背后的游戏规则.md",
unlockAfterDays: 3,
filePath: "book/第二篇|真实的行业/第5章传统行业篇/5.2 旅游供应链:资源越老越值钱.md",
},
{
id: "5.3",
title: "地摊经济数字化一个月900块的餐车生意",
title: "景区联盟:门票不是目的,是流量入口",
price: 1,
isFree: false,
filePath: "book/第二篇|真实的行业/第5章传统行业篇/5.3 地摊经济数字化一个月900块的餐车生意.md",
unlockAfterDays: 3,
filePath: "book/第二篇|真实的行业/第5章传统行业篇/5.3 景区联盟:门票不是目的,是流量入口.md",
},
{
id: "5.4",
title: "不良资产拍卖:我错过的一个亿佣金",
title: "拍卖行抱朴:我人生错过的4件大钱机会(完整版)",
price: 1,
isFree: false,
filePath: "book/第二篇|真实的行业/第5章传统行业篇/5.4 不良资产拍卖:我错过的一个亿佣金.md",
unlockAfterDays: 3,
filePath: "book/第二篇|真实的行业/第5章传统行业篇/5.4 拍卖行抱朴:我人生错过的4件大钱机会(完整版).md",
},
{
id: "5.5",
title: "桶装水李总:跟物业合作的轻资产模式",
title: "飞机票供应链:为什么越便宜越亏",
price: 1,
isFree: false,
filePath: "book/第二篇|真实的行业/第5章传统行业篇/5.5 桶装水李总:跟物业合作的轻资产模式.md",
unlockAfterDays: 3,
filePath: "book/第二篇|真实的行业/第5章传统行业篇/5.5 飞机票供应链:为什么越便宜越亏.md",
},
],
},
@@ -261,31 +291,35 @@ export const bookData: Part[] = [
sections: [
{
id: "6.1",
title: "电商财税窗口:2016年的千万级机会",
title: "错过电商财税(2016-2017)",
price: 1,
isFree: false,
filePath: "book/第三篇|真实的错误/第6章我人生错过的4件大钱/6.1 电商财税窗口2016年的千万级机会.md",
unlockAfterDays: 3,
filePath: "book/第三篇|真实的错误/第6章我人生错过的4件大钱/6.1 错过电商财税(2016-2017).md",
},
{
id: "6.2",
title: "供应链金融:我不懂的杠杆游戏",
title: "错过供应链(2017-2018)",
price: 1,
isFree: false,
filePath: "book/第三篇|真实的错误/第6章我人生错过的4件大钱/6.2 供应链金融:我不懂的杠杆游戏.md",
unlockAfterDays: 3,
filePath: "book/第三篇|真实的错误/第6章我人生错过的4件大钱/6.2 错过供应链(2017-2018).md",
},
{
id: "6.3",
title: "内容红利2019年我为什么没做抖音",
title: "错过内容红利(2018-2019)",
price: 1,
isFree: false,
filePath: "book/第三篇|真实的错误/第6章我人生错过的4件大钱/6.3 内容红利2019年我为什么没做抖音.md",
unlockAfterDays: 3,
filePath: "book/第三篇|真实的错误/第6章我人生错过的4件大钱/6.3 错过内容红利(2018-2019).md",
},
{
id: "6.4",
title: "数据资产化:我还在观望的未来机会",
title: "错过资源资产化(2019-2020)",
price: 1,
isFree: false,
filePath: "book/第三篇|真实的错误/第6章我人生错过的4件大钱/6.4 数据资产化:我还在观望的未来机会.md",
unlockAfterDays: 3,
filePath: "book/第三篇|真实的错误/第6章我人生错过的4件大钱/6.4 错过资源资产化(2019-2020).md",
},
],
},
@@ -295,9 +329,10 @@ export const bookData: Part[] = [
sections: [
{
id: "7.1",
title: "投资房年轻人的迷茫:资金 vs 能力",
title: "投资房年轻人的迷茫:资金vs能力",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第三篇|真实的错误/第7章别人犯的错误/7.1 投资房年轻人的迷茫:资金 vs 能力.md",
},
{
@@ -305,6 +340,7 @@ export const bookData: Part[] = [
title: "信息差骗局:永远有人靠卖学习赚钱",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第三篇|真实的错误/第7章别人犯的错误/7.2 信息差骗局:永远有人靠卖学习赚钱.md",
},
{
@@ -312,6 +348,7 @@ export const bookData: Part[] = [
title: "在Soul找恋爱但想赚钱的人",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第三篇|真实的错误/第7章别人犯的错误/7.3 在Soul找恋爱但想赚钱的人.md",
},
{
@@ -319,6 +356,7 @@ export const bookData: Part[] = [
title: "创业者的三种死法:冲动、轻信、没结构",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第三篇|真实的错误/第7章别人犯的错误/7.4 创业者的三种死法:冲动、轻信、没结构.md",
},
{
@@ -326,6 +364,7 @@ export const bookData: Part[] = [
title: "人情生意的终点:关系越多亏得越多",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第三篇|真实的错误/第7章别人犯的错误/7.5 人情生意的终点:关系越多亏得越多.md",
},
],
@@ -347,6 +386,7 @@ export const bookData: Part[] = [
title: "流量杠杆:抖音、Soul、飞书",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第四篇|真实的赚钱/第8章底层结构/8.1 流量杠杆:抖音、Soul、飞书.md",
},
{
@@ -354,13 +394,15 @@ export const bookData: Part[] = [
title: "价格杠杆:供应链与信息差",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第四篇|真实的赚钱/第8章底层结构/8.2 价格杠杆:供应链与信息差.md",
},
{
id: "8.3",
title: "时间杠杆:自动化 + AI",
title: "时间杠杆:自动化+AI",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第四篇|真实的赚钱/第8章底层结构/8.3 时间杠杆:自动化 + AI.md",
},
{
@@ -368,6 +410,7 @@ export const bookData: Part[] = [
title: "情绪杠杆:咨询、婚恋、生意场",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第四篇|真实的赚钱/第8章底层结构/8.4 情绪杠杆:咨询、婚恋、生意场.md",
},
{
@@ -375,6 +418,7 @@ export const bookData: Part[] = [
title: "社交杠杆:认识谁比你会什么更重要",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第四篇|真实的赚钱/第8章底层结构/8.5 社交杠杆:认识谁比你会什么更重要.md",
},
{
@@ -382,6 +426,7 @@ export const bookData: Part[] = [
title: "云阿米巴:分不属于自己的钱",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第四篇|真实的赚钱/第8章底层结构/8.6 云阿米巴:分不属于自己的钱.md",
},
],
@@ -395,6 +440,7 @@ export const bookData: Part[] = [
title: "游戏账号私域:账号即资产",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例/9.1 游戏账号私域:账号即资产.md",
},
{
@@ -402,6 +448,7 @@ export const bookData: Part[] = [
title: "健康包模式:高复购、高毛利",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例/9.2 健康包模式:高复购、高毛利.md",
},
{
@@ -409,13 +456,15 @@ export const bookData: Part[] = [
title: "药物私域:长期关系赛道",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例/9.3 药物私域:长期关系赛道.md",
},
{
id: "9.4",
title: "残疾机构合作:退税 × AI × 人力成本",
title: "残疾机构合作:退税×AI×人力成本",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath:
"book/第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例/9.4 残疾机构合作:退税 × AI × 人力成本.md",
},
@@ -424,6 +473,7 @@ export const bookData: Part[] = [
title: "私域银行:粉丝即小股东",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例/9.5 私域银行:粉丝即小股东.md",
},
{
@@ -431,6 +481,7 @@ export const bookData: Part[] = [
title: "Soul派对房:陌生人成交的最快场景",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例/9.6 Soul派对房:陌生人成交的最快场景.md",
},
{
@@ -438,62 +489,10 @@ export const bookData: Part[] = [
title: "飞书中台:从聊天到成交的流程化体系",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath:
"book/第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例/9.7 飞书中台:从聊天到成交的流程化体系.md",
},
{
id: "9.8",
title: "餐饮女孩6万营收、1万利润的死撑生意",
price: 1,
isFree: false,
filePath:
"book/第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例/9.8 餐饮女孩6万营收、1万利润的死撑生意.md",
},
{
id: "9.9",
title: "电竞生态:从陪玩到签约到酒店的完整链条",
price: 1,
isFree: false,
filePath:
"book/第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例/9.9 电竞生态:从陪玩到签约到酒店的完整链条.md",
},
{
id: "9.10",
title: "淘客大佬损耗30%的白色通道",
price: 1,
isFree: false,
filePath: "book/第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例/9.10 淘客大佬损耗30%的白色通道.md",
},
{
id: "9.11",
title: "蔬菜供应链:农户才是最赚钱的人",
price: 1,
isFree: false,
filePath: "book/第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例/9.11 蔬菜供应链:农户才是最赚钱的人.md",
},
{
id: "9.12",
title: "美业整合:一个人的公司如何月入十万",
price: 1,
isFree: false,
filePath:
"book/第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例/9.12 美业整合:一个人的公司如何月入十万.md",
},
{
id: "9.13",
title: "AI工具推广一个隐藏的高利润赛道",
price: 1,
isFree: false,
filePath:
"book/第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例/9.13 AI工具推广一个隐藏的高利润赛道.md",
},
{
id: "9.14",
title: "大健康私域一个月150万的70后",
price: 1,
isFree: false,
filePath: "book/第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例/9.14 大健康私域一个月150万的70后.md",
},
],
},
],
@@ -510,23 +509,26 @@ export const bookData: Part[] = [
sections: [
{
id: "10.1",
title: "AI时代:哪些工作会消失,哪些会崛起",
title: "AI代聊与岗位替换",
price: 1,
isFree: false,
filePath: "book/第五篇|真实的社会/第10章未来职业的变化趋势/10.1 AI时代哪些工作会消失哪些会崛起.md",
unlockAfterDays: 3,
filePath: "book/第五篇|真实的社会/第10章未来职业的变化趋势/10.1 AI代聊与岗位替换.md",
},
{
id: "10.2",
title: "一人公司:为什么越来越多人选择单干",
title: "系统化工作vs杂乱工作",
price: 1,
isFree: false,
filePath: "book/第五篇|真实的社会/第10章未来职业的变化趋势/10.2 一人公司:为什么越来越多人选择单干.md",
unlockAfterDays: 3,
filePath: "book/第五篇|真实的社会/第10章未来职业的变化趋势/10.2 系统化工作 vs 杂乱工作.md",
},
{
id: "10.3",
title: "为什么链接能力会成为第一价值",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第五篇|真实的社会/第10章未来职业的变化趋势/10.3 为什么链接能力会成为第一价值.md",
},
{
@@ -534,6 +536,7 @@ export const bookData: Part[] = [
title: "新型公司:Soul-飞书-线下的三位一体",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第五篇|真实的社会/第10章未来职业的变化趋势/10.4 新型公司:Soul-飞书-线下的三位一体.md",
},
],
@@ -544,31 +547,34 @@ export const bookData: Part[] = [
sections: [
{
id: "11.1",
title: "私域经济:为什么流量越来越贵",
title: "城市之间的模式差",
price: 1,
isFree: false,
filePath: "book/第五篇|真实的社会/第11章中国社会商业生态的未来/11.1 私域经济:为什么流量越来越贵.md",
unlockAfterDays: 3,
filePath: "book/第五篇|真实的社会/第11章中国社会商业生态的未来/11.1 城市之间的模式差.md",
},
{
id: "11.2",
title: "银发经济与孤独经济:两个被忽视的万亿市场",
title: "厦门样本:低成本高效率经济",
price: 1,
isFree: false,
filePath:
"book/第五篇|真实的社会/第11章中国社会商业生态的未来/11.2 银发经济与孤独经济:两个被忽视的万亿市场.md",
unlockAfterDays: 3,
filePath: "book/第五篇|真实的社会/第11章中国社会商业生态的未来/11.2 厦门样本:低成本高效率经济.md",
},
{
id: "11.3",
title: "流量红利的终局",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第五篇|真实的社会/第11章中国社会商业生态的未来/11.3 流量红利的终局.md",
},
{
id: "11.4",
title: "大模型 + 供应链的组合拳",
title: "大模型+供应链的组合拳",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第五篇|真实的社会/第11章中国社会商业生态的未来/11.4 大模型 + 供应链的组合拳.md",
},
{
@@ -576,6 +582,7 @@ export const bookData: Part[] = [
title: "社会分层的最终逻辑",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第五篇|真实的社会/第11章中国社会商业生态的未来/11.5 社会分层的最终逻辑.md",
},
],
@@ -594,15 +601,21 @@ export const specialSections = {
},
epilogue: {
id: "epilogue",
title: "尾声|这本书的真实目的",
title: "尾声|终极答案:努力不是关键,选择才是",
price: 0,
isFree: true,
filePath: "book/尾声|这本书的真实目的.md",
filePath: "book/尾声|终极答案:努力不是关键,选择才是.md",
},
}
export const FULL_BOOK_PRICE = getFullBookPrice()
export function getAllSections(): Section[] {
const sections: Section[] = []
if (typeof window !== "undefined") {
const customSections = JSON.parse(localStorage.getItem("custom_sections") || "[]")
sections.push(...customSections)
}
bookData.forEach((part) => {
part.chapters.forEach((chapter) => {
sections.push(...chapter.sections)
@@ -612,8 +625,11 @@ export function getAllSections(): Section[] {
}
export function getSectionById(id: string): Section | undefined {
if (id === "preface") return specialSections.preface as Section
if (id === "epilogue") return specialSections.epilogue as Section
if (typeof window !== "undefined") {
const customSections = JSON.parse(localStorage.getItem("custom_sections") || "[]") as Section[]
const customSection = customSections.find((s) => s.id === id)
if (customSection) return customSection
}
for (const part of bookData) {
for (const chapter of part.chapters) {
@@ -643,3 +659,18 @@ export function isSectionUnlocked(section: Section): boolean {
const unlockDate = new Date(createdDate.getTime() + section.unlockAfterDays * 24 * 60 * 60 * 1000)
return new Date() >= unlockDate
}
export function addCustomSection(section: Omit<Section, "createdAt">): Section {
const newSection: Section = {
...section,
createdAt: new Date().toISOString(),
}
if (typeof window !== "undefined") {
const customSections = JSON.parse(localStorage.getItem("custom_sections") || "[]") as Section[]
customSections.push(newSection)
localStorage.setItem("custom_sections", JSON.stringify(customSections))
}
return newSection
}