338 lines
14 KiB
TypeScript
338 lines
14 KiB
TypeScript
"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 { 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"
|
|
|
|
interface ChapterContentProps {
|
|
section: Section & { filePath: string }
|
|
partTitle: string
|
|
chapterTitle: string
|
|
}
|
|
|
|
export function ChapterContent({ section, partTitle, chapterTitle }: ChapterContentProps) {
|
|
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 isUnlocked = isSectionUnlocked(section)
|
|
const canAccess = section.isFree || isUnlocked || (isLoggedIn && hasPurchased(section.id))
|
|
|
|
useEffect(() => {
|
|
setFullBookPrice(getFullBookPrice())
|
|
}, [])
|
|
|
|
// 阅读进度追踪
|
|
useEffect(() => {
|
|
const handleScroll = () => {
|
|
const scrollTop = window.scrollY
|
|
const docHeight = document.documentElement.scrollHeight - window.innerHeight
|
|
const progress = Math.min((scrollTop / docHeight) * 100, 100)
|
|
setReadingProgress(progress)
|
|
}
|
|
|
|
window.addEventListener('scroll', handleScroll)
|
|
return () => window.removeEventListener('scroll', handleScroll)
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
async function loadContent() {
|
|
try {
|
|
if (section.content) {
|
|
setContent(section.content)
|
|
setIsLoading(false)
|
|
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)
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to load content:", error)
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
loadContent()
|
|
}, [section.filePath, section.id, section.content])
|
|
|
|
const handlePurchaseClick = (type: "section" | "fullbook") => {
|
|
if (!isLoggedIn) {
|
|
setIsAuthOpen(true)
|
|
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)
|
|
|
|
return (
|
|
<div className="min-h-screen bg-black text-white pb-20 page-transition">
|
|
{/* 阅读进度条 */}
|
|
<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"
|
|
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"
|
|
>
|
|
<ChevronLeft className="w-5 h-5 text-[var(--app-text-secondary)]" />
|
|
</Link>
|
|
<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 />
|
|
</div>
|
|
</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">
|
|
{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>
|
|
)}
|
|
</div>
|
|
<h1 className="text-2xl sm:text-3xl font-bold text-white leading-tight tracking-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>
|
|
) : 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>
|
|
</>
|
|
) : (
|
|
<div>
|
|
{/* 预览内容 */}
|
|
<article className="book-content relative">
|
|
<div className="text-[var(--app-text-secondary)] leading-[1.9] text-[17px]">
|
|
{previewContent.split("\n").map((paragraph, index) => (
|
|
paragraph.trim() && (
|
|
<p key={index} className="mb-6 text-justify">
|
|
{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>
|
|
<h3 className="text-2xl font-semibold text-white mb-2">解锁完整内容</h3>
|
|
<p className="text-[var(--app-text-secondary)] mb-8">
|
|
{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">
|
|
<button
|
|
onClick={() => handlePurchaseClick("section")}
|
|
className="btn-ios-secondary px-8 py-4"
|
|
>
|
|
购买本节 ¥{section.price}
|
|
</button>
|
|
<button
|
|
onClick={() => handlePurchaseClick("fullbook")}
|
|
className="btn-ios px-8 py-4 glow flex items-center justify-center gap-2"
|
|
>
|
|
<Sparkles className="w-4 h-4" />
|
|
购买全书 ¥{fullBookPrice.toFixed(1)}
|
|
<span className="text-xs opacity-80 ml-1">省82%</span>
|
|
</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>
|
|
</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)}
|
|
type={paymentType}
|
|
sectionId={section.id}
|
|
sectionTitle={section.title}
|
|
amount={paymentType === "section" ? section.price : fullBookPrice}
|
|
onSuccess={() => window.location.reload()}
|
|
/>
|
|
<QRCodeModal isOpen={isQRModalOpen} onClose={() => setIsQRModalOpen(false)} />
|
|
</div>
|
|
)
|
|
}
|