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:
v0
2026-01-14 05:24:13 +00:00
parent 59ca3b2bbd
commit 1e25c7134a
8 changed files with 783 additions and 1212 deletions

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}