Update homepage layout and navigation to match current trends. #VERCEL_SKIP Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
243 lines
9.3 KiB
TypeScript
243 lines
9.3 KiB
TypeScript
"use client"
|
||
|
||
import { useState, useEffect } from "react"
|
||
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 { PaymentModal } from "./payment-modal"
|
||
import { AuthModal } from "./modules/auth/auth-modal"
|
||
|
||
interface ChapterContentProps {
|
||
section: Section & { filePath: string }
|
||
partTitle: string
|
||
chapterTitle: string
|
||
}
|
||
|
||
export function ChapterContent({ section, partTitle, chapterTitle }: ChapterContentProps) {
|
||
const router = useRouter()
|
||
const [content, setContent] = useState<string>("")
|
||
const [isLoading, setIsLoading] = useState(true)
|
||
const [isPaymentOpen, setIsPaymentOpen] = useState(false)
|
||
const [isAuthOpen, setIsAuthOpen] = useState(false)
|
||
const [paymentType, setPaymentType] = useState<"section" | "fullbook">("section")
|
||
const [readingProgress, setReadingProgress] = useState(0)
|
||
const [showPaywall, setShowPaywall] = useState(false)
|
||
|
||
const { user, isLoggedIn, hasPurchased } = useStore()
|
||
const fullBookPrice = getFullBookPrice()
|
||
const totalSections = getTotalSectionCount()
|
||
|
||
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 = docHeight > 0 ? Math.min((scrollTop / docHeight) * 100, 100) : 0
|
||
setReadingProgress(progress)
|
||
|
||
if (progress >= 20 && !canAccess) {
|
||
setShowPaywall(true)
|
||
}
|
||
}
|
||
|
||
window.addEventListener("scroll", handleScroll)
|
||
return () => window.removeEventListener("scroll", handleScroll)
|
||
}, [canAccess])
|
||
|
||
// 加载内容
|
||
useEffect(() => {
|
||
async function loadContent() {
|
||
try {
|
||
if (section.content) {
|
||
setContent(section.content)
|
||
setIsLoading(false)
|
||
return
|
||
}
|
||
|
||
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, section.content])
|
||
|
||
const handlePurchaseClick = (type: "section" | "fullbook") => {
|
||
if (!isLoggedIn) {
|
||
setIsAuthOpen(true)
|
||
return
|
||
}
|
||
setPaymentType(type)
|
||
setIsPaymentOpen(true)
|
||
}
|
||
|
||
const contentLines = content.split("\n").filter((line) => line.trim())
|
||
const previewLineCount = Math.ceil(contentLines.length * 0.2) // 改为20%
|
||
const previewContent = contentLines.slice(0, previewLineCount).join("\n")
|
||
|
||
return (
|
||
<div className="min-h-screen bg-black text-white">
|
||
<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-[#00CED1] to-[#20B2AA] transition-all duration-150"
|
||
style={{ width: `${readingProgress}%` }}
|
||
/>
|
||
</div>
|
||
|
||
{/* 顶部导航 */}
|
||
<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("/chapters")}
|
||
className="w-9 h-9 rounded-full bg-[#1c1c1e] flex items-center justify-center active:bg-[#2c2c2e]"
|
||
>
|
||
<ChevronLeft className="w-5 h-5 text-gray-400" />
|
||
</button>
|
||
<div className="text-center flex-1 px-4">
|
||
{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 py-8 pb-32">
|
||
<div className="mb-8">
|
||
<div className="flex items-center gap-2 mb-3">
|
||
<span className="text-[#00CED1] text-sm font-medium bg-[#00CED1]/10 px-3 py-1 rounded-full">
|
||
{section.id}
|
||
</span>
|
||
{section.isFree && <span className="text-xs text-[#00CED1] bg-[#00CED1]/10 px-2 py-0.5 rounded">免费</span>}
|
||
</div>
|
||
<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="h-4 bg-[#1c1c1e] rounded animate-pulse"
|
||
style={{ width: `${Math.random() * 40 + 60}%` }}
|
||
/>
|
||
))}
|
||
</div>
|
||
) : canAccess ? (
|
||
// 完整内容
|
||
<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="text-gray-300 leading-[1.9] text-[17px]">
|
||
{previewContent.split("\n").map(
|
||
(paragraph, index) =>
|
||
paragraph.trim() && (
|
||
<p key={index} className="mb-6">
|
||
{paragraph}
|
||
</p>
|
||
),
|
||
)}
|
||
</article>
|
||
|
||
{showPaywall && (
|
||
<>
|
||
{/* 渐变遮罩 */}
|
||
<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-[#00CED1]/20">
|
||
<div className="text-center">
|
||
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-[#00CED1]/10 flex items-center justify-center">
|
||
<Lock className="w-8 h-8 text-[#00CED1]" />
|
||
</div>
|
||
<h3 className="text-xl font-semibold text-white mb-2">解锁完整内容</h3>
|
||
<p className="text-gray-400 text-sm mb-6">
|
||
已阅读20%,{isLoggedIn ? "购买后继续阅读" : "登录并购买后继续阅读"}
|
||
</p>
|
||
|
||
{/* 购买选项 */}
|
||
<div className="space-y-3 mb-6">
|
||
<button
|
||
onClick={() => handlePurchaseClick("section")}
|
||
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"
|
||
>
|
||
<div className="flex items-center justify-between">
|
||
<span>购买本章</span>
|
||
<span className="text-[#00CED1]">¥{section.price}</span>
|
||
</div>
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => handlePurchaseClick("fullbook")}
|
||
className="w-full py-3.5 px-6 rounded-xl bg-gradient-to-r from-[#00CED1] to-[#20B2AA] text-white font-medium active:scale-[0.98] transition-transform shadow-lg shadow-[#00CED1]/20"
|
||
>
|
||
<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-xs text-gray-500">分享给好友购买,你可获得90%佣金</p>
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
</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()}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|