feat: complete product overhaul
Refactor homepage, match feature, data storage, and my page; implement paid reading logic. #VERCEL_SKIP Co-authored-by: undefined <undefined+undefined@users.noreply.github.com>
This commit is contained in:
153
app/login/page.tsx
Normal file
153
app/login/page.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useStore } from "@/lib/store"
|
||||
import { ChevronLeft, Phone, User, Hash } from "lucide-react"
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter()
|
||||
const { login, register, adminLogin } = useStore()
|
||||
const [mode, setMode] = useState<"login" | "register">("login")
|
||||
const [phone, setPhone] = useState("")
|
||||
const [code, setCode] = useState("")
|
||||
const [nickname, setNickname] = useState("")
|
||||
const [referralCode, setReferralCode] = useState("")
|
||||
const [error, setError] = useState("")
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setError("")
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
// 管理员登录
|
||||
if (phone.toLowerCase() === "admin") {
|
||||
if (adminLogin(phone, code)) {
|
||||
router.push("/admin")
|
||||
return
|
||||
} else {
|
||||
setError("管理员密码错误")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (mode === "login") {
|
||||
const success = await login(phone, code)
|
||||
if (success) {
|
||||
router.push("/")
|
||||
} else {
|
||||
setError("验证码错误或用户不存在")
|
||||
}
|
||||
} else {
|
||||
if (!nickname.trim()) {
|
||||
setError("请输入昵称")
|
||||
return
|
||||
}
|
||||
const success = await register(phone, nickname, referralCode || undefined)
|
||||
if (success) {
|
||||
router.push("/")
|
||||
} else {
|
||||
setError("该手机号已注册")
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-white flex flex-col">
|
||||
{/* 顶部导航 */}
|
||||
<header className="flex items-center px-4 py-3">
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="w-9 h-9 rounded-full bg-[#1c1c1e] flex items-center justify-center"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5 text-gray-400" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{/* 主内容 */}
|
||||
<main className="flex-1 px-6 pt-8">
|
||||
<h1 className="text-2xl font-bold mb-2">{mode === "login" ? "登录" : "注册"}</h1>
|
||||
<p className="text-gray-500 text-sm mb-8">
|
||||
{mode === "login" ? "登录后查看购买记录和收益" : "注册后开始阅读真实商业故事"}
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* 手机号 */}
|
||||
<div className="relative">
|
||||
<Phone className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500" />
|
||||
<input
|
||||
type="tel"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
placeholder="手机号"
|
||||
className="w-full pl-12 pr-4 py-3.5 bg-[#1c1c1e] rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[#30d158]/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 昵称(注册时显示) */}
|
||||
{mode === "register" && (
|
||||
<div className="relative">
|
||||
<User className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500" />
|
||||
<input
|
||||
type="text"
|
||||
value={nickname}
|
||||
onChange={(e) => setNickname(e.target.value)}
|
||||
placeholder="昵称"
|
||||
className="w-full pl-12 pr-4 py-3.5 bg-[#1c1c1e] rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[#30d158]/50"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 验证码/密码 */}
|
||||
<div className="relative">
|
||||
<Hash className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500" />
|
||||
<input
|
||||
type="text"
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
placeholder={mode === "login" ? "验证码(测试:123456)" : "设置密码"}
|
||||
className="w-full pl-12 pr-4 py-3.5 bg-[#1c1c1e] rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[#30d158]/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 邀请码(注册时显示) */}
|
||||
{mode === "register" && (
|
||||
<div className="relative">
|
||||
<Hash className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500" />
|
||||
<input
|
||||
type="text"
|
||||
value={referralCode}
|
||||
onChange={(e) => setReferralCode(e.target.value)}
|
||||
placeholder="邀请码(选填)"
|
||||
className="w-full pl-12 pr-4 py-3.5 bg-[#1c1c1e] rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[#30d158]/50"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 错误提示 */}
|
||||
{error && <p className="text-red-500 text-sm">{error}</p>}
|
||||
|
||||
{/* 提交按钮 */}
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={loading || !phone}
|
||||
className="w-full py-3.5 bg-[#30d158] text-white font-medium rounded-xl active:scale-[0.98] transition-transform disabled:opacity-50"
|
||||
>
|
||||
{loading ? "处理中..." : mode === "login" ? "登录" : "注册"}
|
||||
</button>
|
||||
|
||||
{/* 切换模式 */}
|
||||
<div className="text-center">
|
||||
<button onClick={() => setMode(mode === "login" ? "register" : "login")} className="text-[#30d158] text-sm">
|
||||
{mode === "login" ? "没有账号?去注册" : "已有账号?去登录"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
352
app/page.tsx
352
app/page.tsx
@@ -1,9 +1,349 @@
|
||||
import { HomeScreen } from "@/components/home-screen"
|
||||
import { getBookStructure } from "@/lib/book-file-system"
|
||||
"use client"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useStore } from "@/lib/store"
|
||||
import { bookData, getTotalSectionCount, specialSections } from "@/lib/book-data"
|
||||
import { Book, Lock, Unlock, ChevronRight, User, BookOpen } from "lucide-react"
|
||||
|
||||
export default async function HomePage() {
|
||||
const parts = getBookStructure()
|
||||
return <HomeScreen parts={parts} />
|
||||
export default function HomePage() {
|
||||
const router = useRouter()
|
||||
const { user, isLoggedIn, hasPurchased } = useStore()
|
||||
const [activeTab, setActiveTab] = useState<"home" | "me">("home")
|
||||
const [expandedPart, setExpandedPart] = useState<string | null>("part-1")
|
||||
|
||||
const totalSections = getTotalSectionCount()
|
||||
const purchasedCount = user?.purchasedSections?.length || 0
|
||||
const hasFullBook = user?.hasFullBook || false
|
||||
|
||||
// 点击章节
|
||||
const handleSectionClick = (sectionId: string, isFree: boolean) => {
|
||||
if (isFree || hasFullBook || hasPurchased(sectionId)) {
|
||||
router.push(`/read/${sectionId}`)
|
||||
} else {
|
||||
router.push(`/read/${sectionId}`)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-white flex flex-col">
|
||||
{/* 顶部固定区域 - 书籍信息 */}
|
||||
<header className="flex-shrink-0 px-4 pt-safe-top pb-3 bg-gradient-to-b from-black to-transparent">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-14 h-14 rounded-xl bg-gradient-to-br from-[#30d158] to-[#00c7be] flex items-center justify-center shadow-lg">
|
||||
<Book className="w-7 h-7 text-white" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h1 className="text-lg font-bold tracking-tight">一场SOUL的创业实验场</h1>
|
||||
<p className="text-xs text-gray-400 mt-0.5">来自Soul派对房的真实商业故事</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold text-[#30d158]">{totalSections}</div>
|
||||
<div className="text-[10px] text-gray-500">章节</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* 主内容区域 - 可滚动的目录 */}
|
||||
{activeTab === "home" ? (
|
||||
<main className="flex-1 overflow-y-auto px-4 pb-20 scrollbar-hide">
|
||||
{/* 序言入口 */}
|
||||
<button
|
||||
onClick={() => handleSectionClick("preface", true)}
|
||||
className="w-full mb-3 p-3 rounded-xl bg-[#1c1c1e] flex items-center justify-between active:bg-[#2c2c2e] transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-[#30d158]/20 flex items-center justify-center">
|
||||
<BookOpen className="w-4 h-4 text-[#30d158]" />
|
||||
</div>
|
||||
<span className="text-sm font-medium">序言|为什么我每天早上6点在Soul开播?</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-[#30d158]">免费</span>
|
||||
<ChevronRight className="w-4 h-4 text-gray-500" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* 五篇目录 */}
|
||||
{bookData.map((part) => (
|
||||
<div key={part.id} className="mb-3">
|
||||
{/* 篇标题 */}
|
||||
<button
|
||||
onClick={() => setExpandedPart(expandedPart === part.id ? null : part.id)}
|
||||
className="w-full p-3 rounded-xl bg-[#1c1c1e] flex items-center justify-between active:bg-[#2c2c2e] transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-[#30d158] to-[#00c7be] flex items-center justify-center text-sm font-bold">
|
||||
{part.number}
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="text-sm font-semibold">{part.title}</div>
|
||||
<div className="text-[10px] text-gray-500">{part.subtitle}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-500">
|
||||
{part.chapters.reduce((acc, ch) => acc + ch.sections.length, 0)}章
|
||||
</span>
|
||||
<ChevronRight
|
||||
className={`w-4 h-4 text-gray-500 transition-transform ${expandedPart === part.id ? "rotate-90" : ""}`}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* 展开的章节列表 */}
|
||||
{expandedPart === part.id && (
|
||||
<div className="mt-2 ml-2 space-y-1">
|
||||
{part.chapters.map((chapter) => (
|
||||
<div key={chapter.id} className="rounded-lg bg-[#1c1c1e]/50 overflow-hidden">
|
||||
<div className="px-3 py-2 text-xs font-medium text-gray-400 border-b border-white/5">
|
||||
{chapter.title}
|
||||
</div>
|
||||
{chapter.sections.map((section) => {
|
||||
const isPurchased = hasFullBook || hasPurchased(section.id)
|
||||
const canRead = section.isFree || isPurchased
|
||||
|
||||
return (
|
||||
<button
|
||||
key={section.id}
|
||||
onClick={() => handleSectionClick(section.id, section.isFree)}
|
||||
className="w-full px-3 py-2.5 flex items-center justify-between border-b border-white/5 last:border-0 active:bg-white/5 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
{canRead ? (
|
||||
<Unlock className="w-3.5 h-3.5 text-[#30d158] flex-shrink-0" />
|
||||
) : (
|
||||
<Lock className="w-3.5 h-3.5 text-gray-500 flex-shrink-0" />
|
||||
)}
|
||||
<span className={`text-xs truncate ${canRead ? "text-white" : "text-gray-400"}`}>
|
||||
{section.id} {section.title}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0 ml-2">
|
||||
{section.isFree ? (
|
||||
<span className="text-[10px] text-[#30d158] px-1.5 py-0.5 rounded bg-[#30d158]/10">
|
||||
免费
|
||||
</span>
|
||||
) : isPurchased ? (
|
||||
<span className="text-[10px] text-[#30d158]">已购</span>
|
||||
) : (
|
||||
<span className="text-[10px] text-gray-500">¥{section.price}</span>
|
||||
)}
|
||||
<ChevronRight className="w-3.5 h-3.5 text-gray-600" />
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* 尾声入口 */}
|
||||
<button
|
||||
onClick={() => handleSectionClick("epilogue", true)}
|
||||
className="w-full mb-3 p-3 rounded-xl bg-[#1c1c1e] flex items-center justify-between active:bg-[#2c2c2e] transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-[#30d158]/20 flex items-center justify-center">
|
||||
<BookOpen className="w-4 h-4 text-[#30d158]" />
|
||||
</div>
|
||||
<span className="text-sm font-medium">尾声|这本书的真实目的</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-[#30d158]">免费</span>
|
||||
<ChevronRight className="w-4 h-4 text-gray-500" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* 附录 */}
|
||||
<div className="mb-3 p-3 rounded-xl bg-[#1c1c1e]">
|
||||
<div className="text-xs font-medium text-gray-400 mb-2">附录</div>
|
||||
{specialSections.appendix.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => handleSectionClick(item.id, true)}
|
||||
className="w-full py-2 flex items-center justify-between border-b border-white/5 last:border-0 active:bg-white/5"
|
||||
>
|
||||
<span className="text-xs text-gray-300">{item.title}</span>
|
||||
<ChevronRight className="w-3.5 h-3.5 text-gray-600" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
) : (
|
||||
<MyPage />
|
||||
)}
|
||||
|
||||
{/* 底部导航 */}
|
||||
<nav className="flex-shrink-0 fixed bottom-0 left-0 right-0 bg-[#1c1c1e]/90 backdrop-blur-xl border-t border-white/10 pb-safe-bottom">
|
||||
<div className="flex justify-around py-2">
|
||||
<button
|
||||
onClick={() => setActiveTab("home")}
|
||||
className={`flex flex-col items-center gap-0.5 px-6 py-1 ${activeTab === "home" ? "text-[#30d158]" : "text-gray-500"}`}
|
||||
>
|
||||
<Book className="w-5 h-5" />
|
||||
<span className="text-[10px]">首页</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("me")}
|
||||
className={`flex flex-col items-center gap-0.5 px-6 py-1 ${activeTab === "me" ? "text-[#30d158]" : "text-gray-500"}`}
|
||||
>
|
||||
<User className="w-5 h-5" />
|
||||
<span className="text-[10px]">我的</span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function MyPage() {
|
||||
const router = useRouter()
|
||||
const { user, isLoggedIn, logout } = useStore()
|
||||
const [showLogin, setShowLogin] = useState(false)
|
||||
|
||||
const purchasedCount = user?.purchasedSections?.length || 0
|
||||
const hasFullBook = user?.hasFullBook || false
|
||||
const earnings = user?.earnings || 0
|
||||
const totalSections = getTotalSectionCount()
|
||||
|
||||
// 计算阅读进度
|
||||
const readProgress = hasFullBook ? 100 : Math.round((purchasedCount / totalSections) * 100)
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col items-center justify-center px-6 pb-20">
|
||||
<div className="w-20 h-20 rounded-full bg-[#1c1c1e] flex items-center justify-center mb-4">
|
||||
<User className="w-10 h-10 text-gray-500" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold mb-2">登录后查看更多</h2>
|
||||
<p className="text-sm text-gray-500 text-center mb-6">登录后可查看购买记录、阅读进度和收益</p>
|
||||
<button
|
||||
onClick={() => router.push("/login")}
|
||||
className="px-8 py-3 bg-[#30d158] text-white font-medium rounded-xl active:scale-95 transition-transform"
|
||||
>
|
||||
立即登录
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="flex-1 overflow-y-auto px-4 pb-20 scrollbar-hide">
|
||||
{/* 用户信息卡片 */}
|
||||
<div className="p-4 rounded-2xl bg-gradient-to-br from-[#1c1c1e] to-[#2c2c2e] mb-4">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-14 h-14 rounded-full bg-gradient-to-br from-[#30d158] to-[#00c7be] flex items-center justify-center text-xl font-bold">
|
||||
{user?.nickname?.charAt(0) || "U"}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-semibold">{user?.nickname || "用户"}</div>
|
||||
<div className="text-xs text-gray-500">{user?.phone}</div>
|
||||
</div>
|
||||
{hasFullBook && (
|
||||
<div className="px-2 py-1 bg-[#30d158]/20 rounded-lg">
|
||||
<span className="text-xs text-[#30d158] font-medium">全书会员</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 数据统计 */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="p-3 rounded-xl bg-black/30 text-center">
|
||||
<div className="text-xl font-bold text-[#30d158]">{hasFullBook ? totalSections : purchasedCount}</div>
|
||||
<div className="text-[10px] text-gray-500 mt-1">已购章节</div>
|
||||
</div>
|
||||
<div className="p-3 rounded-xl bg-black/30 text-center">
|
||||
<div className="text-xl font-bold text-white">{readProgress}%</div>
|
||||
<div className="text-[10px] text-gray-500 mt-1">阅读进度</div>
|
||||
</div>
|
||||
<div className="p-3 rounded-xl bg-black/30 text-center">
|
||||
<div className="text-xl font-bold text-[#ff9500]">¥{earnings.toFixed(2)}</div>
|
||||
<div className="text-[10px] text-gray-500 mt-1">累计收益</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 阅读进度条 */}
|
||||
<div className="p-4 rounded-xl bg-[#1c1c1e] mb-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-medium">阅读进度</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
{hasFullBook ? totalSections : purchasedCount}/{totalSections}章
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 bg-black/50 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-[#30d158] to-[#00c7be] rounded-full transition-all duration-500"
|
||||
style={{ width: `${readProgress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 收益明细 */}
|
||||
<div className="p-4 rounded-xl bg-[#1c1c1e] mb-4">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<span className="text-sm font-medium">收益明细</span>
|
||||
<button className="text-xs text-[#30d158]">查看详情</button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs text-gray-400">可提现</span>
|
||||
<span className="text-sm font-medium text-[#30d158]">¥{(user?.pendingEarnings || 0).toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs text-gray-400">已提现</span>
|
||||
<span className="text-sm text-gray-400">¥{(user?.withdrawnEarnings || 0).toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs text-gray-400">推荐人数</span>
|
||||
<span className="text-sm text-gray-400">{user?.referralCount || 0}人</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 购买全书入口 */}
|
||||
{!hasFullBook && (
|
||||
<button
|
||||
onClick={() => router.push("/purchase")}
|
||||
className="w-full p-4 rounded-xl bg-gradient-to-r from-[#30d158] to-[#00c7be] mb-4 active:scale-[0.98] transition-transform"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-white">解锁全部 {totalSections} 章</div>
|
||||
<div className="text-xs text-white/70 mt-0.5">一次购买,永久阅读</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold text-white">¥9.9</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 邀请码 */}
|
||||
<div className="p-4 rounded-xl bg-[#1c1c1e] mb-4">
|
||||
<div className="text-sm font-medium mb-2">我的邀请码</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 p-2 bg-black/30 rounded-lg text-center font-mono text-[#30d158]">
|
||||
{user?.referralCode || "N/A"}
|
||||
</div>
|
||||
<button className="px-4 py-2 bg-[#30d158]/20 text-[#30d158] text-sm rounded-lg active:bg-[#30d158]/30">
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-500 mt-2">邀请好友购买,获得90%佣金</p>
|
||||
</div>
|
||||
|
||||
{/* 退出登录 */}
|
||||
<button
|
||||
onClick={logout}
|
||||
className="w-full p-3 rounded-xl bg-[#1c1c1e] text-red-500 text-sm font-medium active:bg-[#2c2c2e] transition-colors"
|
||||
>
|
||||
退出登录
|
||||
</button>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { notFound } from "next/navigation"
|
||||
import { ChapterContent } from "@/components/chapter-content"
|
||||
import { getSectionBySlug, getChapterBySectionSlug } from "@/lib/book-file-system"
|
||||
import { specialSections } from "@/lib/book-data"
|
||||
import { specialSections, getSectionById } from "@/lib/book-data"
|
||||
|
||||
interface ReadPageProps {
|
||||
params: Promise<{ id: string }>
|
||||
@@ -14,25 +14,37 @@ export default async function ReadPage({ params }: ReadPageProps) {
|
||||
const { id } = await params
|
||||
|
||||
if (id === "preface") {
|
||||
return <ChapterContent section={specialSections.preface} partTitle="序言" chapterTitle="" />
|
||||
return <ChapterContent section={specialSections.preface as any} partTitle="序言" chapterTitle="" />
|
||||
}
|
||||
|
||||
if (id === "epilogue") {
|
||||
return <ChapterContent section={specialSections.epilogue} partTitle="尾声" chapterTitle="" />
|
||||
return <ChapterContent section={specialSections.epilogue as any} partTitle="尾声" chapterTitle="" />
|
||||
}
|
||||
|
||||
if (id.startsWith("appendix-")) {
|
||||
const appendixSection = specialSections.appendix.find((a) => a.id === id)
|
||||
if (appendixSection) {
|
||||
return <ChapterContent section={appendixSection as any} partTitle="附录" chapterTitle="" />
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// 先从文件系统获取
|
||||
const section = getSectionBySlug(id)
|
||||
if (!section) {
|
||||
notFound()
|
||||
if (section) {
|
||||
const context = getChapterBySectionSlug(id)
|
||||
if (context) {
|
||||
return <ChapterContent section={section} partTitle={context.part.title} chapterTitle={context.chapter.title} />
|
||||
}
|
||||
}
|
||||
|
||||
const context = getChapterBySectionSlug(id)
|
||||
if (!context) {
|
||||
notFound()
|
||||
// 再从book-data获取
|
||||
const bookSection = getSectionById(id)
|
||||
if (bookSection) {
|
||||
return <ChapterContent section={bookSection as any} partTitle="" chapterTitle="" />
|
||||
}
|
||||
|
||||
return <ChapterContent section={section} partTitle={context.part.title} chapterTitle={context.chapter.title} />
|
||||
notFound()
|
||||
} catch (error) {
|
||||
console.error("[v0] Error in ReadPage:", error)
|
||||
notFound()
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import Link from "next/link"
|
||||
import { ChevronLeft, Lock, Share2, BookOpen, Clock, MessageCircle, ChevronRight, Sparkles } from "lucide-react"
|
||||
import { type Section, getFullBookPrice, isSectionUnlocked } from "@/lib/book-data"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { ChevronLeft, Lock, Share2, Sparkles } from "lucide-react"
|
||||
import { type Section, getFullBookPrice, getTotalSectionCount } from "@/lib/book-data"
|
||||
import { useStore } from "@/lib/store"
|
||||
import { AuthModal } from "./modules/auth/auth-modal"
|
||||
import { PaymentModal } from "./modules/payment/payment-modal"
|
||||
import { UserMenu } from "./user-menu"
|
||||
import { QRCodeModal } from "./modules/marketing/qr-code-modal"
|
||||
import { ReferralShare } from "./modules/referral/referral-share"
|
||||
import { PaymentModal } from "./payment-modal"
|
||||
|
||||
interface ChapterContentProps {
|
||||
section: Section & { filePath: string }
|
||||
@@ -18,38 +14,34 @@ interface ChapterContentProps {
|
||||
}
|
||||
|
||||
export function ChapterContent({ section, partTitle, chapterTitle }: ChapterContentProps) {
|
||||
const router = useRouter()
|
||||
const [content, setContent] = useState<string>("")
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isAuthOpen, setIsAuthOpen] = useState(false)
|
||||
const [isPaymentOpen, setIsPaymentOpen] = useState(false)
|
||||
const [isQRModalOpen, setIsQRModalOpen] = useState(false)
|
||||
const [paymentType, setPaymentType] = useState<"section" | "fullbook">("section")
|
||||
const [fullBookPrice, setFullBookPrice] = useState(9.9)
|
||||
const [readingProgress, setReadingProgress] = useState(0)
|
||||
|
||||
const { user, isLoggedIn, hasPurchased, settings } = useStore()
|
||||
const distributorShare = settings?.distributorShare || 90
|
||||
const { user, isLoggedIn, hasPurchased } = useStore()
|
||||
const fullBookPrice = getFullBookPrice()
|
||||
const totalSections = getTotalSectionCount()
|
||||
|
||||
const isUnlocked = isSectionUnlocked(section)
|
||||
const canAccess = section.isFree || isUnlocked || (isLoggedIn && hasPurchased(section.id))
|
||||
|
||||
useEffect(() => {
|
||||
setFullBookPrice(getFullBookPrice())
|
||||
}, [])
|
||||
const hasFullBook = user?.hasFullBook || false
|
||||
const canAccess = section.isFree || hasFullBook || (isLoggedIn && hasPurchased(section.id))
|
||||
|
||||
// 阅读进度追踪
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const scrollTop = window.scrollY
|
||||
const docHeight = document.documentElement.scrollHeight - window.innerHeight
|
||||
const progress = Math.min((scrollTop / docHeight) * 100, 100)
|
||||
const progress = docHeight > 0 ? Math.min((scrollTop / docHeight) * 100, 100) : 0
|
||||
setReadingProgress(progress)
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
return () => window.removeEventListener('scroll', handleScroll)
|
||||
window.addEventListener("scroll", handleScroll)
|
||||
return () => window.removeEventListener("scroll", handleScroll)
|
||||
}, [])
|
||||
|
||||
// 加载内容
|
||||
useEffect(() => {
|
||||
async function loadContent() {
|
||||
try {
|
||||
@@ -59,22 +51,10 @@ export function ChapterContent({ section, partTitle, chapterTitle }: ChapterCont
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined" && section.filePath.startsWith("custom/")) {
|
||||
const customSections = JSON.parse(localStorage.getItem("custom_sections") || "[]") as Section[]
|
||||
const customSection = customSections.find((s) => s.id === section.id)
|
||||
if (customSection?.content) {
|
||||
setContent(customSection.content)
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/content?path=${encodeURIComponent(section.filePath)}`)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
if (!data.isCustom) {
|
||||
setContent(data.content)
|
||||
}
|
||||
setContent(data.content || "")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load content:", error)
|
||||
@@ -84,244 +64,162 @@ export function ChapterContent({ section, partTitle, chapterTitle }: ChapterCont
|
||||
}
|
||||
|
||||
loadContent()
|
||||
}, [section.filePath, section.id, section.content])
|
||||
}, [section.filePath, section.content])
|
||||
|
||||
const handlePurchaseClick = (type: "section" | "fullbook") => {
|
||||
if (!isLoggedIn) {
|
||||
setIsAuthOpen(true)
|
||||
router.push("/login")
|
||||
return
|
||||
}
|
||||
setPaymentType(type)
|
||||
setIsPaymentOpen(true)
|
||||
}
|
||||
|
||||
const handleShare = async () => {
|
||||
const url = user?.referralCode ? `${window.location.href}?ref=${user.referralCode}` : window.location.href
|
||||
const shareData = {
|
||||
title: section.title,
|
||||
text: `来自Soul派对房的真实商业故事: ${section.title}`,
|
||||
url: url,
|
||||
}
|
||||
|
||||
try {
|
||||
if (navigator.share && navigator.canShare && navigator.canShare(shareData)) {
|
||||
await navigator.share(shareData)
|
||||
} else {
|
||||
navigator.clipboard.writeText(url)
|
||||
alert(
|
||||
`链接已复制!分享后他人购买,你可获得${distributorShare}%返利 (¥${((fullBookPrice * distributorShare) / 100).toFixed(1)})`,
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
if ((error as Error).name !== "AbortError") {
|
||||
navigator.clipboard.writeText(url)
|
||||
alert(`链接已复制!分享后他人购买,你可获得${distributorShare}%返利`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const previewContent = content.slice(0, 500)
|
||||
// 计算预览内容(前50%)
|
||||
const contentLines = content.split("\n").filter((line) => line.trim())
|
||||
const previewLineCount = Math.ceil(contentLines.length * 0.5)
|
||||
const previewContent = contentLines.slice(0, previewLineCount).join("\n")
|
||||
const hiddenContent = contentLines.slice(previewLineCount).join("\n")
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-white pb-20 page-transition">
|
||||
<div className="min-h-screen bg-black text-white">
|
||||
{/* 阅读进度条 */}
|
||||
<div className="fixed top-0 left-0 right-0 z-[60] h-0.5 bg-[var(--app-bg-secondary)]">
|
||||
<div
|
||||
className="h-full bg-[var(--app-brand)] transition-all duration-150"
|
||||
<div className="fixed top-0 left-0 right-0 z-50 h-0.5 bg-[#1c1c1e]">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-[#30d158] to-[#00c7be] transition-all duration-150"
|
||||
style={{ width: `${readingProgress}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Header - iOS风格毛玻璃 */}
|
||||
<header className="sticky top-0 z-50 glass-nav safe-top">
|
||||
<div className="max-w-3xl mx-auto px-4 py-3 flex items-center justify-between">
|
||||
<Link
|
||||
href="/chapters"
|
||||
className="w-9 h-9 rounded-full bg-[var(--app-bg-secondary)] flex items-center justify-center touch-feedback"
|
||||
{/* 顶部导航 */}
|
||||
<header className="sticky top-0 z-40 bg-black/80 backdrop-blur-xl border-b border-white/5">
|
||||
<div className="max-w-2xl mx-auto px-4 py-3 flex items-center justify-between">
|
||||
<button
|
||||
onClick={() => router.push("/")}
|
||||
className="w-9 h-9 rounded-full bg-[#1c1c1e] flex items-center justify-center active:bg-[#2c2c2e]"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5 text-[var(--app-text-secondary)]" />
|
||||
</Link>
|
||||
<ChevronLeft className="w-5 h-5 text-gray-400" />
|
||||
</button>
|
||||
<div className="text-center flex-1 px-4">
|
||||
<p className="text-[var(--app-text-tertiary)] text-xs">{partTitle}</p>
|
||||
{chapterTitle && (
|
||||
<p className="text-[var(--app-text-secondary)] text-sm truncate">{chapterTitle}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ReferralShare
|
||||
sectionTitle={section.title}
|
||||
fullBookPrice={fullBookPrice}
|
||||
distributorShare={distributorShare}
|
||||
/>
|
||||
<UserMenu />
|
||||
{partTitle && <p className="text-[10px] text-gray-500">{partTitle}</p>}
|
||||
{chapterTitle && <p className="text-xs text-gray-400 truncate">{chapterTitle}</p>}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const url = window.location.href
|
||||
navigator.clipboard.writeText(url)
|
||||
alert("链接已复制")
|
||||
}}
|
||||
className="w-9 h-9 rounded-full bg-[#1c1c1e] flex items-center justify-center active:bg-[#2c2c2e]"
|
||||
>
|
||||
<Share2 className="w-4 h-4 text-gray-400" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* 阅读内容 */}
|
||||
<main className="max-w-2xl mx-auto px-5 sm:px-6 py-8">
|
||||
{/* 标题区域 */}
|
||||
<div className="mb-10">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<span className="text-[var(--app-brand)] text-sm font-medium bg-[var(--app-brand-light)] px-3 py-1 rounded-full">
|
||||
<main className="max-w-2xl mx-auto px-5 py-8 pb-32">
|
||||
{/* 标题 */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-[#30d158] text-sm font-medium bg-[#30d158]/10 px-3 py-1 rounded-full">
|
||||
{section.id}
|
||||
</span>
|
||||
{section.unlockAfterDays && !section.isFree && (
|
||||
<span className="px-3 py-1 bg-[var(--ios-orange)]/20 text-[var(--ios-orange)] text-xs rounded-full flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
{isUnlocked ? "已免费解锁" : `${section.unlockAfterDays}天后免费`}
|
||||
</span>
|
||||
)}
|
||||
{section.isFree && <span className="text-xs text-[#30d158] bg-[#30d158]/10 px-2 py-0.5 rounded">免费</span>}
|
||||
</div>
|
||||
<h1 className="text-2xl sm:text-3xl font-bold text-white leading-tight tracking-tight">
|
||||
{section.title}
|
||||
</h1>
|
||||
<h1 className="text-2xl font-bold text-white leading-tight">{section.title}</h1>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
// 骨架屏加载
|
||||
<div className="space-y-4">
|
||||
{[...Array(8)].map((_, i) => (
|
||||
<div key={i} className="skeleton h-4 rounded" style={{ width: `${Math.random() * 40 + 60}%` }} />
|
||||
<div
|
||||
key={i}
|
||||
className="h-4 bg-[#1c1c1e] rounded animate-pulse"
|
||||
style={{ width: `${Math.random() * 40 + 60}%` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : canAccess ? (
|
||||
<>
|
||||
{/* 正文内容 - 书籍阅读风格 */}
|
||||
<article className="book-content">
|
||||
<div className="text-[var(--app-text-secondary)] leading-[1.9] text-[17px]">
|
||||
{content.split("\n").map((paragraph, index) => (
|
||||
paragraph.trim() && (
|
||||
<p key={index} className="mb-6 text-justify">
|
||||
{paragraph}
|
||||
</p>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
{/* 进群引导 CTA */}
|
||||
<div className="mt-16 glass-card-heavy p-6 overflow-hidden relative">
|
||||
<div className="absolute top-0 right-0 w-24 h-24 bg-[var(--app-brand)] opacity-[0.1] blur-[40px] rounded-full" />
|
||||
<div className="relative flex items-center gap-4">
|
||||
<div className="w-14 h-14 rounded-2xl bg-[var(--app-brand-light)] flex items-center justify-center flex-shrink-0">
|
||||
<MessageCircle className="w-7 h-7 text-[var(--app-brand)]" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-white font-semibold mb-1">想听更多商业故事?</h3>
|
||||
<p className="text-[var(--app-text-tertiary)] text-sm">每天早上6-9点,卡若在Soul派对房分享真实案例</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsQRModalOpen(true)}
|
||||
className="btn-ios whitespace-nowrap flex-shrink-0"
|
||||
>
|
||||
加入派对群
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
// 完整内容
|
||||
<article className="text-gray-300 leading-[1.9] text-[17px]">
|
||||
{content.split("\n").map(
|
||||
(paragraph, index) =>
|
||||
paragraph.trim() && (
|
||||
<p key={index} className="mb-6">
|
||||
{paragraph}
|
||||
</p>
|
||||
),
|
||||
)}
|
||||
</article>
|
||||
) : (
|
||||
// 付费墙:前半免费,后半付费
|
||||
<div>
|
||||
{/* 预览内容 */}
|
||||
<article className="book-content relative">
|
||||
<div className="text-[var(--app-text-secondary)] leading-[1.9] text-[17px]">
|
||||
{previewContent.split("\n").map((paragraph, index) => (
|
||||
{/* 免费预览部分 */}
|
||||
<article className="text-gray-300 leading-[1.9] text-[17px]">
|
||||
{previewContent.split("\n").map(
|
||||
(paragraph, index) =>
|
||||
paragraph.trim() && (
|
||||
<p key={index} className="mb-6 text-justify">
|
||||
<p key={index} className="mb-6">
|
||||
{paragraph}
|
||||
</p>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-0 right-0 h-48 bg-gradient-to-t from-black to-transparent" />
|
||||
),
|
||||
)}
|
||||
</article>
|
||||
|
||||
{/* 购买提示 - 毛玻璃风格 */}
|
||||
<div className="mt-8 glass-card-heavy p-8 text-center overflow-hidden relative">
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-40 h-40 bg-[var(--app-brand)] opacity-[0.08] blur-[60px] rounded-full" />
|
||||
|
||||
<div className="relative">
|
||||
<div className="w-20 h-20 mx-auto mb-6 rounded-2xl bg-[var(--app-bg-secondary)] flex items-center justify-center">
|
||||
<Lock className="w-10 h-10 text-[var(--app-text-tertiary)]" />
|
||||
{/* 渐变遮罩 */}
|
||||
<div className="relative">
|
||||
<div className="absolute -top-32 left-0 right-0 h-32 bg-gradient-to-t from-black to-transparent pointer-events-none" />
|
||||
</div>
|
||||
|
||||
{/* 付费提示卡片 */}
|
||||
<div className="mt-8 p-6 rounded-2xl bg-gradient-to-b from-[#1c1c1e] to-[#2c2c2e] border border-white/10">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-[#30d158]/10 flex items-center justify-center">
|
||||
<Lock className="w-8 h-8 text-[#30d158]" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-semibold text-white mb-2">解锁完整内容</h3>
|
||||
<p className="text-[var(--app-text-secondary)] mb-8">
|
||||
{isLoggedIn ? "购买本节或整本书以阅读完整内容" : "登录后购买即可阅读完整内容"}
|
||||
<h3 className="text-xl font-semibold text-white mb-2">解锁完整内容</h3>
|
||||
<p className="text-gray-400 text-sm mb-6">
|
||||
已阅读50%,{isLoggedIn ? "购买后继续阅读" : "登录并购买后继续阅读"}
|
||||
</p>
|
||||
|
||||
{section.unlockAfterDays && (
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 mb-6 rounded-full bg-[var(--ios-orange)]/10 text-[var(--ios-orange)]">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span className="text-sm">本节将在{section.unlockAfterDays}天后免费解锁</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center mb-6">
|
||||
{/* 购买选项 */}
|
||||
<div className="space-y-3 mb-6">
|
||||
<button
|
||||
onClick={() => handlePurchaseClick("section")}
|
||||
className="btn-ios-secondary px-8 py-4"
|
||||
className="w-full py-3.5 px-6 rounded-xl bg-[#2c2c2e] border border-white/10 text-white font-medium active:scale-[0.98] transition-transform"
|
||||
>
|
||||
购买本节 ¥{section.price}
|
||||
<div className="flex items-center justify-between">
|
||||
<span>购买本章</span>
|
||||
<span className="text-[#30d158]">¥{section.price}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handlePurchaseClick("fullbook")}
|
||||
className="btn-ios px-8 py-4 glow flex items-center justify-center gap-2"
|
||||
className="w-full py-3.5 px-6 rounded-xl bg-gradient-to-r from-[#30d158] to-[#00c7be] text-white font-medium active:scale-[0.98] transition-transform shadow-lg shadow-[#30d158]/20"
|
||||
>
|
||||
<Sparkles className="w-4 h-4" />
|
||||
购买全书 ¥{fullBookPrice.toFixed(1)}
|
||||
<span className="text-xs opacity-80 ml-1">省82%</span>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4" />
|
||||
<span>解锁全部 {totalSections} 章</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="text-lg font-bold">¥{fullBookPrice}</span>
|
||||
<span className="text-xs ml-1 opacity-70">省82%</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-[var(--app-text-tertiary)] text-sm">
|
||||
分享本书,他人购买你可获得 <span className="text-[var(--app-brand)]">{distributorShare}%返利</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 进群引导 */}
|
||||
<div className="mt-8 glass-card p-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-14 h-14 rounded-2xl bg-[var(--app-brand-light)] flex items-center justify-center flex-shrink-0">
|
||||
<MessageCircle className="w-7 h-7 text-[var(--app-brand)]" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-white font-semibold mb-1">不想花钱?来派对群免费听!</h3>
|
||||
<p className="text-[var(--app-text-tertiary)] text-sm">每天早上6-9点,卡若在Soul派对房免费分享</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsQRModalOpen(true)}
|
||||
className="btn-ios whitespace-nowrap flex-shrink-0"
|
||||
>
|
||||
加入
|
||||
</button>
|
||||
<p className="text-xs text-gray-500">分享给好友购买,你可获得90%佣金</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 底部导航 */}
|
||||
<div className="mt-16 pt-8 border-t border-[var(--app-separator)] flex justify-between items-center">
|
||||
<Link
|
||||
href="/chapters"
|
||||
className="flex items-center gap-2 text-[var(--app-text-secondary)] hover:text-white transition-colors touch-feedback"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
<span>返回目录</span>
|
||||
</Link>
|
||||
<button
|
||||
onClick={handleShare}
|
||||
className="flex items-center gap-2 text-[var(--app-brand)] touch-feedback"
|
||||
>
|
||||
<Share2 className="w-4 h-4" />
|
||||
<span>分享赚 ¥{((section.price * distributorShare) / 100).toFixed(1)}</span>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* 弹窗 */}
|
||||
<AuthModal isOpen={isAuthOpen} onClose={() => setIsAuthOpen(false)} />
|
||||
{/* 支付弹窗 */}
|
||||
<PaymentModal
|
||||
isOpen={isPaymentOpen}
|
||||
onClose={() => setIsPaymentOpen(false)}
|
||||
@@ -331,7 +229,6 @@ export function ChapterContent({ section, partTitle, chapterTitle }: ChapterCont
|
||||
amount={paymentType === "section" ? section.price : fullBookPrice}
|
||||
onSuccess={() => window.location.reload()}
|
||||
/>
|
||||
<QRCodeModal isOpen={isQRModalOpen} onClose={() => setIsQRModalOpen(false)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,532 +0,0 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
307
lib/book-data.ts
307
lib/book-data.ts
@@ -24,12 +24,9 @@ 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 function getFullBookPrice(sectionsCount?: number): number {
|
||||
export function getFullBookPrice(): number {
|
||||
return 9.9
|
||||
}
|
||||
|
||||
@@ -46,40 +43,39 @@ 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,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/_第一篇|真实的人/第1章|人与人之间的底层逻辑/1.2 老墨:资源整合高手的社交方法.md",
|
||||
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: "为什么99%的合作死在沟通差而不是能力差",
|
||||
title: "沟通差的问题:为什么你说的别人听不懂",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
filePath: "book/_第一篇|真实的人/第1章|人与人之间的底层逻辑/1.5 为什么99%的合作死在沟通差而不是能力差.md",
|
||||
filePath: "book/第一篇|真实的人/第1章|人与人之间的底层逻辑/1.5 沟通差的问题:为什么你说的别人听不懂.md",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -92,42 +88,36 @@ export const bookData: Part[] = [
|
||||
title: "相亲故事:你以为找的是人,实际是在找模式",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/_第一篇|真实的人/第2章|人性困境案例/2.1 相亲故事:你以为找的是人,实际是在找模式.md",
|
||||
filePath: "book/第一篇|真实的人/第2章|人性困境案例/2.1 相亲故事:你以为找的是人,实际是在找模式.md",
|
||||
},
|
||||
{
|
||||
id: "2.2",
|
||||
title: "找工作迷茫者:为什么简历解决不了人生",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/_第一篇|真实的人/第2章|人性困境案例/2.2 找工作迷茫者:为什么简历解决不了人生.md",
|
||||
filePath: "book/第一篇|真实的人/第2章|人性困境案例/2.2 找工作迷茫者:为什么简历解决不了人生.md",
|
||||
},
|
||||
{
|
||||
id: "2.3",
|
||||
title: "撸运费险:小钱困住大脑的真实心理",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/_第一篇|真实的人/第2章|人性困境案例/2.3 撸运费险:小钱困住大脑的真实心理.md",
|
||||
filePath: "book/第一篇|真实的人/第2章|人性困境案例/2.3 撸运费险:小钱困住大脑的真实心理.md",
|
||||
},
|
||||
{
|
||||
id: "2.4",
|
||||
title: "游戏上瘾的年轻人:不是游戏吸引他,是生活没吸引力",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath:
|
||||
"book/_第一篇|真实的人/第2章|人性困境案例/2.4 游戏上瘾的年轻人:不是游戏吸引他,是生活没吸引力.md",
|
||||
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",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -145,43 +135,31 @@ export const bookData: Part[] = [
|
||||
sections: [
|
||||
{
|
||||
id: "3.1",
|
||||
title: "电商财税窗口:我错过的第一桶金",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第二篇|真实的行业/第3章|电商篇/3.1 电商财税窗口:我错过的第一桶金.md",
|
||||
},
|
||||
{
|
||||
id: "3.2",
|
||||
title: "3000万流水如何跑出来(退税模式解析)",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第二篇|真实的行业/第3章|电商篇/3.2 3000万流水如何跑出来(退税模式解析).md",
|
||||
filePath: "book/第二篇|真实的行业/第3章|电商篇/3.1 3000万流水如何跑出来(退税模式解析).md",
|
||||
},
|
||||
{
|
||||
id: "3.2",
|
||||
title: "供应链之王 vs 打工人:利润不在前端",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
filePath: "book/第二篇|真实的行业/第3章|电商篇/3.2 供应链之王 vs 打工人:利润不在前端.md",
|
||||
},
|
||||
{
|
||||
id: "3.3",
|
||||
title: "供应链之王vs打工人:利润不在前端",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
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",
|
||||
filePath: "book/第二篇|真实的行业/第3章|电商篇/3.3 社区团购的底层逻辑.md",
|
||||
},
|
||||
{
|
||||
id: "3.5",
|
||||
id: "3.4",
|
||||
title: "跨境电商与退税套利",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第二篇|真实的行业/第3章|电商篇/3.5 跨境电商与退税套利.md",
|
||||
filePath: "book/第二篇|真实的行业/第3章|电商篇/3.4 跨境电商与退税套利.md",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -194,7 +172,6 @@ export const bookData: Part[] = [
|
||||
title: "旅游号:30天10万粉的真实逻辑",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第二篇|真实的行业/第4章|内容商业篇/4.1 旅游号:30天10万粉的真实逻辑.md",
|
||||
},
|
||||
{
|
||||
@@ -202,7 +179,6 @@ export const bookData: Part[] = [
|
||||
title: "做号工厂:如何让一个号变成一个机器",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第二篇|真实的行业/第4章|内容商业篇/4.2 做号工厂:如何让一个号变成一个机器.md",
|
||||
},
|
||||
{
|
||||
@@ -210,7 +186,6 @@ export const bookData: Part[] = [
|
||||
title: "情绪内容为什么比专业内容更赚钱",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第二篇|真实的行业/第4章|内容商业篇/4.3 情绪内容为什么比专业内容更赚钱.md",
|
||||
},
|
||||
{
|
||||
@@ -218,7 +193,6 @@ export const bookData: Part[] = [
|
||||
title: "猫与宠物号:为什么宠物赛道永不过时",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第二篇|真实的行业/第4章|内容商业篇/4.4 猫与宠物号:为什么宠物赛道永不过时.md",
|
||||
},
|
||||
{
|
||||
@@ -226,7 +200,6 @@ export const bookData: Part[] = [
|
||||
title: "直播间里的三种人:演员、技术工、系统流",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第二篇|真实的行业/第4章|内容商业篇/4.5 直播间里的三种人:演员、技术工、系统流.md",
|
||||
},
|
||||
],
|
||||
@@ -237,43 +210,38 @@ export const bookData: Part[] = [
|
||||
sections: [
|
||||
{
|
||||
id: "5.1",
|
||||
title: "羽毛球馆:为什么体育培训是最稳定的现金流",
|
||||
title: "拍卖行抱朴:一天240万的摇号生意",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第二篇|真实的行业/第5章|传统行业篇/5.1 羽毛球馆:为什么体育培训是最稳定的现金流.md",
|
||||
filePath: "book/第二篇|真实的行业/第5章|传统行业篇/5.1 拍卖行抱朴:一天240万的摇号生意.md",
|
||||
},
|
||||
{
|
||||
id: "5.2",
|
||||
title: "旅游供应链:资源越老越值钱",
|
||||
title: "土地拍卖:招拍挂背后的游戏规则",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第二篇|真实的行业/第5章|传统行业篇/5.2 旅游供应链:资源越老越值钱.md",
|
||||
filePath: "book/第二篇|真实的行业/第5章|传统行业篇/5.2 土地拍卖:招拍挂背后的游戏规则.md",
|
||||
},
|
||||
{
|
||||
id: "5.3",
|
||||
title: "景区联盟:门票不是目的,是流量入口",
|
||||
title: "地摊经济数字化:一个月900块的餐车生意",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第二篇|真实的行业/第5章|传统行业篇/5.3 景区联盟:门票不是目的,是流量入口.md",
|
||||
filePath: "book/第二篇|真实的行业/第5章|传统行业篇/5.3 地摊经济数字化:一个月900块的餐车生意.md",
|
||||
},
|
||||
{
|
||||
id: "5.4",
|
||||
title: "拍卖行抱朴:我人生错过的4件大钱机会(完整版)",
|
||||
title: "不良资产拍卖:我错过的一个亿佣金",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第二篇|真实的行业/第5章|传统行业篇/5.4 拍卖行抱朴:我人生错过的4件大钱机会(完整版).md",
|
||||
filePath: "book/第二篇|真实的行业/第5章|传统行业篇/5.4 不良资产拍卖:我错过的一个亿佣金.md",
|
||||
},
|
||||
{
|
||||
id: "5.5",
|
||||
title: "飞机票供应链:为什么越便宜越亏",
|
||||
title: "桶装水李总:跟物业合作的轻资产模式",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第二篇|真实的行业/第5章|传统行业篇/5.5 飞机票供应链:为什么越便宜越亏.md",
|
||||
filePath: "book/第二篇|真实的行业/第5章|传统行业篇/5.5 桶装水李总:跟物业合作的轻资产模式.md",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -291,35 +259,31 @@ export const bookData: Part[] = [
|
||||
sections: [
|
||||
{
|
||||
id: "6.1",
|
||||
title: "错过电商财税(2016-2017)",
|
||||
title: "电商财税窗口:2016年的千万级机会",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第三篇|真实的错误/第6章|我人生错过的4件大钱/6.1 错过电商财税(2016-2017).md",
|
||||
filePath: "book/第三篇|真实的错误/第6章|我人生错过的4件大钱/6.1 电商财税窗口:2016年的千万级机会.md",
|
||||
},
|
||||
{
|
||||
id: "6.2",
|
||||
title: "错过供应链(2017-2018)",
|
||||
title: "供应链金融:我不懂的杠杆游戏",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第三篇|真实的错误/第6章|我人生错过的4件大钱/6.2 错过供应链(2017-2018).md",
|
||||
filePath: "book/第三篇|真实的错误/第6章|我人生错过的4件大钱/6.2 供应链金融:我不懂的杠杆游戏.md",
|
||||
},
|
||||
{
|
||||
id: "6.3",
|
||||
title: "错过内容红利(2018-2019)",
|
||||
title: "内容红利:2019年我为什么没做抖音",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第三篇|真实的错误/第6章|我人生错过的4件大钱/6.3 错过内容红利(2018-2019).md",
|
||||
filePath: "book/第三篇|真实的错误/第6章|我人生错过的4件大钱/6.3 内容红利:2019年我为什么没做抖音.md",
|
||||
},
|
||||
{
|
||||
id: "6.4",
|
||||
title: "错过资源资产化(2019-2020)",
|
||||
title: "数据资产化:我还在观望的未来机会",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第三篇|真实的错误/第6章|我人生错过的4件大钱/6.4 错过资源资产化(2019-2020).md",
|
||||
filePath: "book/第三篇|真实的错误/第6章|我人生错过的4件大钱/6.4 数据资产化:我还在观望的未来机会.md",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -329,10 +293,9 @@ 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",
|
||||
},
|
||||
{
|
||||
@@ -340,7 +303,6 @@ export const bookData: Part[] = [
|
||||
title: "信息差骗局:永远有人靠卖学习赚钱",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第三篇|真实的错误/第7章|别人犯的错误/7.2 信息差骗局:永远有人靠卖学习赚钱.md",
|
||||
},
|
||||
{
|
||||
@@ -348,7 +310,6 @@ export const bookData: Part[] = [
|
||||
title: "在Soul找恋爱但想赚钱的人",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第三篇|真实的错误/第7章|别人犯的错误/7.3 在Soul找恋爱但想赚钱的人.md",
|
||||
},
|
||||
{
|
||||
@@ -356,7 +317,6 @@ export const bookData: Part[] = [
|
||||
title: "创业者的三种死法:冲动、轻信、没结构",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第三篇|真实的错误/第7章|别人犯的错误/7.4 创业者的三种死法:冲动、轻信、没结构.md",
|
||||
},
|
||||
{
|
||||
@@ -364,7 +324,6 @@ export const bookData: Part[] = [
|
||||
title: "人情生意的终点:关系越多亏得越多",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第三篇|真实的错误/第7章|别人犯的错误/7.5 人情生意的终点:关系越多亏得越多.md",
|
||||
},
|
||||
],
|
||||
@@ -386,7 +345,6 @@ export const bookData: Part[] = [
|
||||
title: "流量杠杆:抖音、Soul、飞书",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第四篇|真实的赚钱/第8章|底层结构/8.1 流量杠杆:抖音、Soul、飞书.md",
|
||||
},
|
||||
{
|
||||
@@ -394,15 +352,13 @@ 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",
|
||||
},
|
||||
{
|
||||
@@ -410,7 +366,6 @@ export const bookData: Part[] = [
|
||||
title: "情绪杠杆:咨询、婚恋、生意场",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第四篇|真实的赚钱/第8章|底层结构/8.4 情绪杠杆:咨询、婚恋、生意场.md",
|
||||
},
|
||||
{
|
||||
@@ -418,7 +373,6 @@ export const bookData: Part[] = [
|
||||
title: "社交杠杆:认识谁比你会什么更重要",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第四篇|真实的赚钱/第8章|底层结构/8.5 社交杠杆:认识谁比你会什么更重要.md",
|
||||
},
|
||||
{
|
||||
@@ -426,7 +380,6 @@ export const bookData: Part[] = [
|
||||
title: "云阿米巴:分不属于自己的钱",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第四篇|真实的赚钱/第8章|底层结构/8.6 云阿米巴:分不属于自己的钱.md",
|
||||
},
|
||||
],
|
||||
@@ -440,7 +393,6 @@ export const bookData: Part[] = [
|
||||
title: "游戏账号私域:账号即资产",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第四篇|真实的赚钱/第9章|我在Soul上亲访的赚钱案例/9.1 游戏账号私域:账号即资产.md",
|
||||
},
|
||||
{
|
||||
@@ -448,7 +400,6 @@ export const bookData: Part[] = [
|
||||
title: "健康包模式:高复购、高毛利",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第四篇|真实的赚钱/第9章|我在Soul上亲访的赚钱案例/9.2 健康包模式:高复购、高毛利.md",
|
||||
},
|
||||
{
|
||||
@@ -456,15 +407,13 @@ 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",
|
||||
},
|
||||
@@ -473,7 +422,6 @@ export const bookData: Part[] = [
|
||||
title: "私域银行:粉丝即小股东",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第四篇|真实的赚钱/第9章|我在Soul上亲访的赚钱案例/9.5 私域银行:粉丝即小股东.md",
|
||||
},
|
||||
{
|
||||
@@ -481,7 +429,6 @@ export const bookData: Part[] = [
|
||||
title: "Soul派对房:陌生人成交的最快场景",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第四篇|真实的赚钱/第9章|我在Soul上亲访的赚钱案例/9.6 Soul派对房:陌生人成交的最快场景.md",
|
||||
},
|
||||
{
|
||||
@@ -489,10 +436,62 @@ 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",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -509,26 +508,23 @@ export const bookData: Part[] = [
|
||||
sections: [
|
||||
{
|
||||
id: "10.1",
|
||||
title: "AI代聊与岗位替换",
|
||||
title: "AI时代:哪些工作会消失,哪些会崛起",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第五篇|真实的社会/第10章|未来职业的变化趋势/10.1 AI代聊与岗位替换.md",
|
||||
filePath: "book/第五篇|真实的社会/第10章|未来职业的变化趋势/10.1 AI时代:哪些工作会消失,哪些会崛起.md",
|
||||
},
|
||||
{
|
||||
id: "10.2",
|
||||
title: "系统化工作vs杂乱工作",
|
||||
title: "一人公司:为什么越来越多人选择单干",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第五篇|真实的社会/第10章|未来职业的变化趋势/10.2 系统化工作 vs 杂乱工作.md",
|
||||
filePath: "book/第五篇|真实的社会/第10章|未来职业的变化趋势/10.2 一人公司:为什么越来越多人选择单干.md",
|
||||
},
|
||||
{
|
||||
id: "10.3",
|
||||
title: "为什么链接能力会成为第一价值",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第五篇|真实的社会/第10章|未来职业的变化趋势/10.3 为什么链接能力会成为第一价值.md",
|
||||
},
|
||||
{
|
||||
@@ -536,7 +532,6 @@ export const bookData: Part[] = [
|
||||
title: "新型公司:Soul-飞书-线下的三位一体",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第五篇|真实的社会/第10章|未来职业的变化趋势/10.4 新型公司:Soul-飞书-线下的三位一体.md",
|
||||
},
|
||||
],
|
||||
@@ -547,34 +542,31 @@ export const bookData: Part[] = [
|
||||
sections: [
|
||||
{
|
||||
id: "11.1",
|
||||
title: "城市之间的模式差",
|
||||
title: "私域经济:为什么流量越来越贵",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第五篇|真实的社会/第11章|中国社会商业生态的未来/11.1 城市之间的模式差.md",
|
||||
filePath: "book/第五篇|真实的社会/第11章|中国社会商业生态的未来/11.1 私域经济:为什么流量越来越贵.md",
|
||||
},
|
||||
{
|
||||
id: "11.2",
|
||||
title: "厦门样本:低成本高效率经济",
|
||||
title: "银发经济与孤独经济:两个被忽视的万亿市场",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第五篇|真实的社会/第11章|中国社会商业生态的未来/11.2 厦门样本:低成本高效率经济.md",
|
||||
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",
|
||||
},
|
||||
{
|
||||
@@ -582,7 +574,6 @@ export const bookData: Part[] = [
|
||||
title: "社会分层的最终逻辑",
|
||||
price: 1,
|
||||
isFree: false,
|
||||
unlockAfterDays: 3,
|
||||
filePath: "book/第五篇|真实的社会/第11章|中国社会商业生态的未来/11.5 社会分层的最终逻辑.md",
|
||||
},
|
||||
],
|
||||
@@ -591,6 +582,7 @@ export const bookData: Part[] = [
|
||||
},
|
||||
]
|
||||
|
||||
// 特殊章节:序言、尾声、附录
|
||||
export const specialSections = {
|
||||
preface: {
|
||||
id: "preface",
|
||||
@@ -601,21 +593,49 @@ export const specialSections = {
|
||||
},
|
||||
epilogue: {
|
||||
id: "epilogue",
|
||||
title: "尾声|终极答案:努力不是关键,选择才是",
|
||||
title: "尾声|这本书的真实目的",
|
||||
price: 0,
|
||||
isFree: true,
|
||||
filePath: "book/尾声|终极答案:努力不是关键,选择才是.md",
|
||||
filePath: "book/尾声|这本书的真实目的.md",
|
||||
},
|
||||
appendix: [
|
||||
{
|
||||
id: "appendix-1",
|
||||
title: "附录1|Soul派对房精选对话",
|
||||
price: 0,
|
||||
isFree: true,
|
||||
filePath: "book/附录/附录1|Soul派对房精选对话.md",
|
||||
},
|
||||
{
|
||||
id: "appendix-2",
|
||||
title: "附录2|创业者自检清单",
|
||||
price: 0,
|
||||
isFree: true,
|
||||
filePath: "book/附录/附录2|创业者自检清单.md",
|
||||
},
|
||||
{
|
||||
id: "appendix-3",
|
||||
title: "附录3|本书提到的工具和资源",
|
||||
price: 0,
|
||||
isFree: true,
|
||||
filePath: "book/附录/附录3|本书提到的工具和资源.md",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const FULL_BOOK_PRICE = getFullBookPrice()
|
||||
// 获取总章节数
|
||||
export function getTotalSectionCount(): number {
|
||||
let count = 0
|
||||
bookData.forEach((part) => {
|
||||
part.chapters.forEach((chapter) => {
|
||||
count += chapter.sections.length
|
||||
})
|
||||
})
|
||||
return count // 64章
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -625,18 +645,17 @@ export function getAllSections(): Section[] {
|
||||
}
|
||||
|
||||
export function getSectionById(id: string): Section | undefined {
|
||||
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) {
|
||||
const section = chapter.sections.find((s) => s.id === id)
|
||||
if (section) return section
|
||||
}
|
||||
}
|
||||
// 检查特殊章节
|
||||
if (id === "preface") return specialSections.preface as Section
|
||||
if (id === "epilogue") return specialSections.epilogue as Section
|
||||
const appendix = specialSections.appendix.find((a) => a.id === id)
|
||||
if (appendix) return appendix as Section
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -651,26 +670,4 @@ export function getChapterBySection(sectionId: string): { part: Part; chapter: C
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function isSectionUnlocked(section: Section): boolean {
|
||||
if (section.isFree) return true
|
||||
if (!section.unlockAfterDays || !section.createdAt) return false
|
||||
|
||||
const createdDate = new Date(section.createdAt)
|
||||
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
|
||||
}
|
||||
export const FULL_BOOK_PRICE = getFullBookPrice()
|
||||
|
||||
Reference in New Issue
Block a user