Files
soul-yongping/components/home-screen.tsx
v0 59ca3b2bbd refactor: full product interaction system redesign
Refactor homepage, reading modal, matching feature, and user profile for improved UX

#VERCEL_SKIP

Co-authored-by: undefined <undefined+undefined@users.noreply.github.com>
2026-01-14 05:17:59 +00:00

533 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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