Refactor homepage, reading modal, matching feature, and user profile for improved UX #VERCEL_SKIP Co-authored-by: undefined <undefined+undefined@users.noreply.github.com>
533 lines
17 KiB
TypeScript
533 lines
17 KiB
TypeScript
"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>
|
||
)
|
||
}
|