删除多个完成报告文件,优化项目结构以提升可维护性。

This commit is contained in:
2026-02-03 15:59:37 +08:00
parent d4ca9573f5
commit a2443c097c
119 changed files with 2119 additions and 8537 deletions

123
app/view/about/page.tsx Normal file
View File

@@ -0,0 +1,123 @@
"use client"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { Clock, MessageCircle, BookOpen, Users, Award, TrendingUp, ArrowLeft } from "lucide-react"
import { QRCodeModal } from "@/components/modules/marketing/qr-code-modal"
import { useStore } from "@/lib/store"
export default function AboutPage() {
const router = useRouter()
const [showQRModal, setShowQRModal] = useState(false)
const { settings } = useStore()
const authorInfo = settings?.authorInfo || {
name: "卡若",
description: "连续创业者,私域运营专家",
liveTime: "06:00-09:00",
platform: "Soul派对房",
}
const stats = [
{ icon: BookOpen, value: "55+", label: "真实案例" },
{ icon: Users, value: "10000+", label: "派对房听众" },
{ icon: Award, value: "15年", label: "创业经验" },
{ icon: TrendingUp, value: "3000万", label: "最高年流水" },
]
const milestones = [
{ year: "2007-2014", event: "游戏电竞创业历程,从魔兽世界代练起步" },
{ year: "2015", event: "转型电商,做天猫虚拟充值" },
{ year: "2016-2019", event: "深耕电商领域团队扩张到200人年流水3000万" },
{ year: "2019-2020", event: "公司变故,重整旗鼓" },
{ year: "2020-2025", event: "电竞、地摊、大健康、私域多领域探索" },
{ year: "2025.10.15", event: "在Soul派对房开启每日分享记录真实商业案例" },
]
return (
<div className="min-h-screen bg-black text-white pb-8">
<header className="sticky top-0 z-40 bg-black/90 backdrop-blur-xl border-b border-white/5">
<div className="px-4 py-3 flex items-center">
<button onClick={() => router.back()} className="p-2 -ml-2 rounded-full hover:bg-white/5">
<ArrowLeft className="w-5 h-5 text-white" />
</button>
<h1 className="text-lg font-semibold text-[#00CED1] flex-1 text-center pr-7"></h1>
</div>
</header>
<main className="px-4 py-6 space-y-5">
<div className="p-5 rounded-2xl bg-gradient-to-br from-[#1c1c1e] to-[#2c2c2e] border border-[#00CED1]/20">
<div className="flex flex-col items-center text-center">
<div className="w-20 h-20 rounded-full bg-gradient-to-br from-[#00CED1] to-[#20B2AA] flex items-center justify-center text-3xl font-bold text-white mb-4">
{authorInfo.name.charAt(0)}
</div>
<h2 className="text-xl font-bold text-white">{authorInfo.name}</h2>
<p className="text-gray-400 text-sm mt-1">{authorInfo.description}</p>
<div className="flex items-center gap-4 mt-4">
<span className="flex items-center gap-1 text-[#00CED1] text-xs bg-[#00CED1]/10 px-3 py-1.5 rounded-full">
<Clock className="w-3 h-3" />
{authorInfo.liveTime}
</span>
<span className="flex items-center gap-1 text-gray-400 text-xs bg-white/5 px-3 py-1.5 rounded-full">
<MessageCircle className="w-3 h-3" />
{authorInfo.platform}
</span>
</div>
</div>
</div>
<div className="grid grid-cols-4 gap-2">
{stats.map((stat, index) => (
<div key={index} className="p-3 rounded-xl bg-[#1c1c1e] border border-white/5 text-center">
<stat.icon className="w-5 h-5 text-[#00CED1] mx-auto mb-2" />
<p className="text-base font-bold text-white">{stat.value}</p>
<p className="text-gray-500 text-[10px]">{stat.label}</p>
</div>
))}
</div>
{/* 关于这本书 */}
<div className="p-5 rounded-2xl bg-[#1c1c1e] border border-white/5">
<h3 className="text-base font-semibold text-white mb-3"></h3>
<div className="space-y-2 text-gray-300 text-sm leading-relaxed">
<p>"这不是一本教你成功的鸡汤书。"</p>
<p>69,Soul派对房和几百个陌生人分享的真实故事</p>
<p className="text-[#00CED1] font-medium">"社会不是靠努力,是靠洞察与选择。"</p>
</div>
</div>
<div className="p-5 rounded-2xl bg-[#1c1c1e] border border-white/5">
<h3 className="text-base font-semibold text-white mb-3"></h3>
<div className="space-y-3">
{milestones.map((item, index) => (
<div key={index} className="flex gap-3">
<div className="flex flex-col items-center">
<div className="w-2 h-2 rounded-full bg-[#00CED1]" />
{index < milestones.length - 1 && <div className="w-0.5 flex-1 bg-gray-700 mt-1" />}
</div>
<div className="pb-3 flex-1">
<p className="text-[#00CED1] font-semibold text-sm">{item.year}</p>
<p className="text-gray-300 text-xs mt-0.5">{item.event}</p>
</div>
</div>
))}
</div>
</div>
<div className="p-5 rounded-2xl bg-gradient-to-r from-[#00CED1]/10 to-[#20B2AA]/10 border border-[#00CED1]/20">
<h3 className="text-base font-semibold text-white mb-2">?</h3>
<p className="text-gray-400 text-sm mb-4">6-9,Soul派对房免费分享</p>
<button
onClick={() => setShowQRModal(true)}
className="w-full py-3 rounded-xl bg-[#00CED1] text-white font-medium flex items-center justify-center gap-2 active:scale-[0.98] transition-transform"
>
<MessageCircle className="w-4 h-4" />
</button>
</div>
</main>
<QRCodeModal isOpen={showQRModal} onClose={() => setShowQRModal(false)} />
</div>
)
}

214
app/view/chapters/page.tsx Normal file
View File

@@ -0,0 +1,214 @@
"use client"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { ChevronRight, Lock, Unlock, Book, BookOpen, Sparkles, Zap, Crown, Search } from "lucide-react"
import { useStore } from "@/lib/store"
import { bookData, getTotalSectionCount, specialSections, getPremiumBookPrice, getExtraSectionsCount, BASE_SECTIONS_COUNT } from "@/lib/book-data"
import { SearchModal } from "@/components/search-modal"
export default function ChaptersPage() {
const router = useRouter()
const { user, hasPurchased } = useStore()
const [expandedPart, setExpandedPart] = useState<string | null>("part-1")
const [bookVersion, setBookVersion] = useState<"basic" | "premium">("basic")
const [showPremiumTab, setShowPremiumTab] = useState(false) // 控制是否显示最新完整版标签
const [searchOpen, setSearchOpen] = useState(false)
const totalSections = getTotalSectionCount()
const hasFullBook = user?.hasFullBook || false
const premiumPrice = getPremiumBookPrice()
const extraSections = getExtraSectionsCount()
const handleSectionClick = (sectionId: string) => {
router.push(`/view/read/${sectionId}`)
}
return (
<div className="min-h-screen bg-black text-white pb-24">
<header className="sticky top-0 z-40 bg-black/90 backdrop-blur-xl border-b border-white/5">
<div className="px-4 py-3 flex items-center justify-between">
<div className="w-8" /> {/* 占位 */}
<h1 className="text-lg font-semibold text-[#00CED1]"></h1>
<button
onClick={() => setSearchOpen(true)}
className="w-8 h-8 rounded-full bg-[#2c2c2e] flex items-center justify-center hover:bg-[#3c3c3e] transition-colors"
>
<Search className="w-4 h-4 text-gray-400" />
</button>
</div>
</header>
{/* 搜索弹窗 */}
<SearchModal open={searchOpen} onOpenChange={setSearchOpen} />
<div className="mx-4 mt-4 p-4 rounded-2xl bg-gradient-to-br from-[#1c1c1e] to-[#2c2c2e] border border-[#00CED1]/20">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-[#00CED1] to-[#20B2AA] flex items-center justify-center">
<Book className="w-6 h-6 text-white" />
</div>
<div className="flex-1">
<h2 className="text-white font-semibold">SOUL的创业实验场</h2>
<p className="text-gray-500 text-xs mt-0.5">Soul派对房的真实商业故事</p>
</div>
<div className="text-right">
<div className="text-xl font-bold text-[#00CED1]">{totalSections}</div>
<div className="text-[10px] text-gray-500"></div>
</div>
</div>
</div>
{/* 版本标签切换 - 仅在showPremiumTab为true时显示 */}
{showPremiumTab && extraSections > 0 && (
<div className="mx-4 mt-3 flex gap-2">
<button
onClick={() => setBookVersion("basic")}
className={`flex-1 py-2.5 rounded-xl text-sm font-medium transition-all ${
bookVersion === "basic"
? "bg-[#00CED1]/20 text-[#00CED1] border border-[#00CED1]/30"
: "bg-[#1c1c1e] text-white/60 border border-white/5"
}`}
>
¥9.9
</button>
<button
onClick={() => setBookVersion("premium")}
className={`flex-1 py-2.5 rounded-xl text-sm font-medium transition-all flex items-center justify-center gap-1 ${
bookVersion === "premium"
? "bg-[#FFD700]/20 text-[#FFD700] border border-[#FFD700]/30"
: "bg-[#1c1c1e] text-white/60 border border-white/5"
}`}
>
<Crown className="w-4 h-4" />
¥{premiumPrice.toFixed(1)}
</button>
</div>
)}
{/* 目录内容 */}
<main className="px-4 py-4">
<button
onClick={() => handleSectionClick("preface")}
className="w-full mb-3 p-3 rounded-xl bg-[#1c1c1e] flex items-center justify-between active:bg-[#2c2c2e] transition-colors border border-white/5"
>
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-[#00CED1]/20 flex items-center justify-center">
<BookOpen className="w-4 h-4 text-[#00CED1]" />
</div>
<span className="text-sm font-medium text-white">6Soul开播?</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-[#00CED1]"></span>
<ChevronRight className="w-4 h-4 text-gray-500" />
</div>
</button>
{bookData.map((part) => (
<div key={part.id} className="mb-3">
<button
onClick={() => setExpandedPart(expandedPart === part.id ? null : part.id)}
className="w-full p-3 rounded-xl bg-[#1c1c1e] flex items-center justify-between active:bg-[#2c2c2e] transition-colors border border-white/5"
>
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-[#00CED1] to-[#20B2AA] flex items-center justify-center text-sm font-bold text-white">
{part.number}
</div>
<div className="text-left">
<div className="text-sm font-semibold text-white">{part.title}</div>
<div className="text-[10px] text-gray-500">{part.subtitle}</div>
</div>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500">
{part.chapters.reduce((acc, ch) => acc + ch.sections.length, 0)}
</span>
<ChevronRight
className={`w-4 h-4 text-gray-500 transition-transform ${expandedPart === part.id ? "rotate-90" : ""}`}
/>
</div>
</button>
{expandedPart === part.id && (
<div className="mt-2 ml-2 space-y-1">
{part.chapters.map((chapter) => (
<div key={chapter.id} className="rounded-lg bg-[#1c1c1e]/50 overflow-hidden border border-white/5">
<div className="px-3 py-2 text-xs font-medium text-gray-400 border-b border-white/5">
{chapter.title}
</div>
{chapter.sections.map((section) => {
const isPurchased = hasFullBook || hasPurchased(section.id)
const canRead = section.isFree || isPurchased
return (
<button
key={section.id}
onClick={() => handleSectionClick(section.id)}
className="w-full px-3 py-2.5 flex items-center justify-between border-b border-white/5 last:border-0 active:bg-white/5 transition-colors"
>
<div className="flex items-center gap-2 flex-1 min-w-0">
{canRead ? (
<Unlock className="w-3.5 h-3.5 text-[#00CED1] flex-shrink-0" />
) : (
<Lock className="w-3.5 h-3.5 text-gray-500 flex-shrink-0" />
)}
<span className={`text-xs truncate ${canRead ? "text-white" : "text-gray-400"}`}>
{section.id} {section.title}
</span>
</div>
<div className="flex items-center gap-2 flex-shrink-0 ml-2">
{section.isFree ? (
<span className="text-[10px] text-[#00CED1] px-1.5 py-0.5 rounded bg-[#00CED1]/10">
</span>
) : isPurchased ? (
<span className="text-[10px] text-[#00CED1]"></span>
) : (
<span className="text-[10px] text-gray-500">¥{section.price}</span>
)}
<ChevronRight className="w-3.5 h-3.5 text-gray-600" />
</div>
</button>
)
})}
</div>
))}
</div>
)}
</div>
))}
<button
onClick={() => handleSectionClick("epilogue")}
className="w-full mb-3 p-3 rounded-xl bg-[#1c1c1e] flex items-center justify-between active:bg-[#2c2c2e] transition-colors border border-white/5"
>
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-[#00CED1]/20 flex items-center justify-center">
<BookOpen className="w-4 h-4 text-[#00CED1]" />
</div>
<span className="text-sm font-medium text-white"></span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-[#00CED1]"></span>
<ChevronRight className="w-4 h-4 text-gray-500" />
</div>
</button>
{/* 附录 */}
<div className="mb-3 p-3 rounded-xl bg-[#1c1c1e] border border-white/5">
<div className="text-xs font-medium text-gray-400 mb-2"></div>
{specialSections.appendix.map((item) => (
<button
key={item.id}
onClick={() => handleSectionClick(item.id)}
className="w-full py-2 flex items-center justify-between border-b border-white/5 last:border-0 active:bg-white/5"
>
<span className="text-xs text-gray-300">{item.title}</span>
<ChevronRight className="w-3.5 h-3.5 text-gray-600" />
</button>
))}
</div>
</main>
</div>
)
}

112
app/view/docs/page.tsx Normal file
View File

@@ -0,0 +1,112 @@
"use client"
import Link from "next/link"
import { ArrowLeft, CreditCard, Share2, FileText, Code } from "lucide-react"
export default function DocsPage() {
return (
<main className="min-h-screen bg-[#0a1628] text-white pb-20">
<div className="sticky top-0 z-10 bg-[#0a1628]/95 backdrop-blur-md border-b border-gray-700/50">
<div className="max-w-2xl mx-auto flex items-center gap-4 p-4">
<Link href="/view" className="p-2 -ml-2">
<ArrowLeft className="w-5 h-5" />
</Link>
<h1 className="text-lg font-semibold"></h1>
</div>
</div>
<div className="p-4">
<div className="max-w-2xl mx-auto space-y-6">
{/* Payment Configuration */}
<section className="bg-[#0f2137]/60 rounded-xl p-6">
<div className="flex items-center gap-3 mb-4">
<CreditCard className="w-6 h-6 text-[#38bdac]" />
<h2 className="text-xl font-semibold"></h2>
</div>
<div className="space-y-4 text-gray-300 text-sm">
<div>
<h3 className="text-white font-medium mb-2"></h3>
<p className="mb-2">1. AppID和AppSecret</p>
<p className="mb-2">2. AppID和AppSecret</p>
<p className="mb-2">3. API密钥</p>
<div className="bg-[#0a1628] rounded-lg p-3 mt-2">
<code className="text-xs text-gray-400">
{`网站AppID: wx432c93e275548671
服务号AppID: wx7c0dbf34ddba300d
商户号: 1318592501`}
</code>
</div>
</div>
<div>
<h3 className="text-white font-medium mb-2"></h3>
<p className="mb-2">1. PID</p>
<p className="mb-2">2. Key</p>
<p className="mb-2">3. </p>
<div className="bg-[#0a1628] rounded-lg p-3 mt-2">
<code className="text-xs text-gray-400">
{`合作者身份(PID): 2088511801157159
安全校验码(Key): lz6ey1h3kl9...`}
</code>
</div>
</div>
</div>
</section>
{/* Distribution System */}
<section className="bg-[#0f2137]/60 rounded-xl p-6">
<div className="flex items-center gap-3 mb-4">
<Share2 className="w-6 h-6 text-[#38bdac]" />
<h2 className="text-xl font-semibold"></h2>
</div>
<div className="space-y-4 text-gray-300 text-sm">
<p>(0-100%)</p>
<div className="bg-[#0a1628] rounded-lg p-4">
<p className="text-white mb-2">:</p>
<code className="text-[#38bdac]"> = × %</code>
</div>
<p>: 用户A通过B的邀请码购买¥9.9,90%</p>
<p>B获得 9.9 × 90% = ¥8.91 </p>
</div>
</section>
{/* Withdrawal */}
<section className="bg-[#0f2137]/60 rounded-xl p-6">
<div className="flex items-center gap-3 mb-4">
<FileText className="w-6 h-6 text-[#38bdac]" />
<h2 className="text-xl font-semibold"></h2>
</div>
<div className="space-y-3 text-gray-300 text-sm">
<p>1. : ¥10</p>
<p>2. 提现周期: T+1</p>
<p>3. 支持提现方式: 微信</p>
<p>4. 提现手续费: 0%</p>
</div>
</section>
{/* API Reference */}
<section className="bg-[#0f2137]/60 rounded-xl p-6">
<div className="flex items-center gap-3 mb-4">
<Code className="w-6 h-6 text-[#38bdac]" />
<h2 className="text-xl font-semibold">API接口</h2>
</div>
<div className="space-y-4 text-gray-300 text-sm">
<div className="bg-[#0a1628] rounded-lg p-4">
<p className="text-gray-400 mb-2"></p>
<code className="text-[#38bdac]">GET /api/content?id=1.1</code>
</div>
<div className="bg-[#0a1628] rounded-lg p-4">
<p className="text-gray-400 mb-2"></p>
<code className="text-[#38bdac]">POST /api/feishu/sync</code>
</div>
</div>
</section>
</div>
</div>
</main>
)
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

View File

@@ -0,0 +1,78 @@
"use client"
import { useEffect, useMemo, useState } from "react"
import { useSearchParams } from "next/navigation"
export default function DocumentationCapturePage() {
const searchParams = useSearchParams()
const path = searchParams.get("path") || "/"
const [loaded, setLoaded] = useState(false)
const [timeoutReached, setTimeoutReached] = useState(false)
const [loadError, setLoadError] = useState<string | null>(null)
const src = useMemo(() => {
if (!path.startsWith("/")) return `/${path}`
return path
}, [path])
useEffect(() => {
setLoaded(false)
setTimeoutReached(false)
setLoadError(null)
const timer = window.setTimeout(() => {
if (!loaded) {
setTimeoutReached(true)
}
}, 60000)
return () => window.clearTimeout(timer)
}, [src, loaded])
const handleLoad = () => {
setLoaded(true)
setTimeoutReached(false)
}
const handleError = () => {
setLoadError("页面加载失败")
}
return (
<main className="min-h-screen bg-white flex items-center justify-center">
<div className="w-[430px] h-[932px] border border-gray-200 bg-white relative overflow-hidden">
<iframe
data-doc-iframe="true"
data-loaded={loaded ? "true" : "false"}
src={src}
className="w-full h-full border-0"
onLoad={handleLoad}
onError={handleError}
title={`Capture: ${path}`}
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
/>
{!loaded && !timeoutReached && !loadError && (
<div className="absolute inset-0 bg-white flex items-center justify-center">
<div className="text-center">
<div className="w-8 h-8 border-2 border-gray-300 border-t-gray-600 rounded-full animate-spin mx-auto mb-2" />
<p className="text-sm text-gray-500">...</p>
</div>
</div>
)}
</div>
{(timeoutReached || loadError) && (
<div className="fixed left-0 top-0 right-0 bg-red-600 text-white text-sm px-3 py-2 text-center">
{loadError || "页面加载超时"}
</div>
)}
{loaded && (
<div className="fixed left-0 bottom-0 right-0 bg-green-600 text-white text-xs px-3 py-1 text-center">
: {path}
</div>
)}
</main>
)
}

View File

@@ -0,0 +1,306 @@
"use client"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { getDocumentationCatalog, type DocumentationPage } from "@/lib/documentation/catalog"
import { FileText, Download, Loader2, CheckCircle, XCircle, Eye, RefreshCw } from "lucide-react"
type PageStatus = "pending" | "loading" | "success" | "error"
type PageState = {
page: DocumentationPage
status: PageStatus
error?: string
}
export default function DocumentationToolPage() {
const [isGenerating, setIsGenerating] = useState(false)
const [error, setError] = useState<string | null>(null)
const [progress, setProgress] = useState(0)
const [currentPage, setCurrentPage] = useState<string | null>(null)
const [pageStates, setPageStates] = useState<PageState[]>([])
const [previewPath, setPreviewPath] = useState<string | null>(null)
const [showPreview, setShowPreview] = useState(false)
const iframeRef = useRef<HTMLIFrameElement>(null)
const pages = useMemo(() => getDocumentationCatalog(), [])
const groupedPages = useMemo(() => {
const groups: Record<string, DocumentationPage[]> = {}
for (const page of pages) {
if (!groups[page.group]) groups[page.group] = []
groups[page.group].push(page)
}
return groups
}, [pages])
useEffect(() => {
setPageStates(pages.map((page) => ({ page, status: "pending" })))
}, [pages])
const handleGenerate = async () => {
setError(null)
setIsGenerating(true)
setProgress(0)
setCurrentPage(null)
setPageStates(pages.map((page) => ({ page, status: "loading" })))
try {
// Simulate progress while waiting for the API
let progressValue = 0
const progressInterval = setInterval(() => {
progressValue += 2
const pageIndex = Math.floor((progressValue / 100) * pages.length)
const nextPage = pages[Math.min(pageIndex, pages.length - 1)]
if (nextPage) setCurrentPage(nextPage.title)
setProgress(Math.min(progressValue, 90))
// Update page states to show progress
setPageStates((prev) =>
prev.map((s, idx) => ({
...s,
status: idx < pageIndex ? "success" : idx === pageIndex ? "loading" : "pending",
})),
)
}, 800)
// Get token from URL if provided
const urlParams = new URLSearchParams(window.location.search)
const token = urlParams.get("token") || ""
const response = await fetch(`/api/documentation/generate${token ? `?token=${token}` : ""}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(token ? { "x-documentation-token": token } : {}),
},
})
clearInterval(progressInterval)
if (!response.ok) {
const text = await response.text().catch(() => "")
let errorMessage = `生成失败(${response.status})`
try {
const json = JSON.parse(text)
errorMessage = json.error || errorMessage
} catch {
if (text) errorMessage = text
}
throw new Error(errorMessage)
}
setProgress(100)
setCurrentPage("完成")
setPageStates(pages.map((page) => ({ page, status: "success" })))
const blob = await response.blob()
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = `应用功能文档_${new Date().toISOString().slice(0, 10)}.docx`
document.body.appendChild(a)
a.click()
a.remove()
URL.revokeObjectURL(url)
} catch (e) {
const message = e instanceof Error ? e.message : String(e)
setError(message)
setPageStates((prev) => prev.map((s) => ({ ...s, status: "error" })))
} finally {
setIsGenerating(false)
}
}
const handlePreview = useCallback((path: string) => {
setPreviewPath(path)
setShowPreview(true)
}, [])
const getStatusIcon = (status: PageStatus) => {
switch (status) {
case "pending":
return <div className="w-4 h-4 rounded-full bg-gray-600" />
case "loading":
return <Loader2 className="w-4 h-4 animate-spin text-teal-400" />
case "success":
return <CheckCircle className="w-4 h-4 text-green-500" />
case "error":
return <XCircle className="w-4 h-4 text-red-500" />
}
}
return (
<main className="min-h-screen bg-background text-foreground p-4 pb-24">
<div className="max-w-md mx-auto space-y-4">
{/* Header */}
<div className="flex items-center gap-3 py-2">
<FileText className="w-6 h-6 text-teal-400" />
<div>
<h1 className="text-lg font-semibold"></h1>
<p className="text-xs text-muted-foreground"></p>
</div>
</div>
{/* Info Card */}
<div className="bg-card border border-border rounded-xl p-4 space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground"></span>
<span className="font-medium text-teal-400">{pages.length} </span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground"></span>
<span className="font-medium">{Object.keys(groupedPages).length} </span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground"></span>
<span className="font-medium">Word (.docx)</span>
</div>
</div>
{/* Progress */}
{isGenerating && (
<div className="bg-card border border-border rounded-xl p-4 space-y-3">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground"></span>
<span className="font-medium text-teal-400">{Math.round(progress)}%</span>
</div>
<div className="w-full h-2 bg-muted rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-teal-500 to-cyan-400 transition-all duration-300"
style={{ width: `${progress}%` }}
/>
</div>
{currentPage && <p className="text-xs text-muted-foreground truncate">: {currentPage}</p>}
</div>
)}
{/* Error */}
{error && (
<div className="bg-destructive/10 border border-destructive/30 text-destructive rounded-xl p-3 text-sm">
<p className="font-medium mb-1"></p>
<p className="text-xs opacity-80">{error}</p>
<p className="text-xs mt-2 opacity-60">提示: 如需授权,URL中添加 ?token=your_token</p>
</div>
)}
{/* Generate Button */}
<button
type="button"
onClick={handleGenerate}
disabled={isGenerating}
className="w-full bg-gradient-to-r from-teal-500 to-cyan-500 text-white rounded-xl py-3.5 font-medium disabled:opacity-60 flex items-center justify-center gap-2 shadow-lg shadow-teal-500/20"
>
{isGenerating ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
<span>...</span>
</>
) : (
<>
<Download className="w-5 h-5" />
<span> Word </span>
</>
)}
</button>
{/* Page List */}
<div className="space-y-3">
<h2 className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<span></span>
<span className="text-xs opacity-60">({pages.length})</span>
</h2>
{Object.entries(groupedPages).map(([group, groupPages]) => (
<div key={group} className="bg-card border border-border rounded-xl overflow-hidden">
<div className="px-3 py-2 bg-muted/50 border-b border-border">
<h3 className="text-sm font-medium text-teal-400">{group}</h3>
</div>
<div className="divide-y divide-border">
{groupPages.map((page, index) => {
const state = pageStates.find((s) => s.page.path === page.path)
return (
<div
key={page.path}
className="px-3 py-2.5 flex items-center gap-3 hover:bg-muted/30 transition-colors"
>
<span className="text-xs text-muted-foreground w-5">{index + 1}</span>
{state && getStatusIcon(state.status)}
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{page.title}</p>
{page.subtitle && <p className="text-xs text-muted-foreground truncate">{page.subtitle}</p>}
</div>
<button
type="button"
onClick={() => handlePreview(page.path)}
className="p-1.5 text-muted-foreground hover:text-foreground hover:bg-muted rounded-lg transition-colors"
title="预览页面"
>
<Eye className="w-4 h-4" />
</button>
</div>
)
})}
</div>
</div>
))}
</div>
{/* Features */}
<div className="bg-card border border-border rounded-xl p-4 space-y-3">
<h3 className="text-sm font-medium"></h3>
<ul className="text-xs text-muted-foreground space-y-2">
<li className="flex items-start gap-2">
<CheckCircle className="w-4 h-4 text-green-500 flex-shrink-0 mt-0.5" />
<span></span>
</li>
<li className="flex items-start gap-2">
<CheckCircle className="w-4 h-4 text-green-500 flex-shrink-0 mt-0.5" />
<span>(iPhone 14 Pro Max尺寸)</span>
</li>
<li className="flex items-start gap-2">
<CheckCircle className="w-4 h-4 text-green-500 flex-shrink-0 mt-0.5" />
<span></span>
</li>
<li className="flex items-start gap-2">
<CheckCircle className="w-4 h-4 text-green-500 flex-shrink-0 mt-0.5" />
<span></span>
</li>
</ul>
</div>
{/* Note */}
<p className="text-xs text-muted-foreground text-center">30-60</p>
</div>
{/* Preview Modal */}
{showPreview && previewPath && (
<div className="fixed inset-0 bg-black/80 z-50 flex items-center justify-center p-4">
<div className="bg-card rounded-2xl w-full max-w-md overflow-hidden border border-border">
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
<h3 className="font-medium text-sm"></h3>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => iframeRef.current?.contentWindow?.location.reload()}
className="p-1.5 text-muted-foreground hover:text-foreground rounded-lg"
>
<RefreshCw className="w-4 h-4" />
</button>
<button
type="button"
onClick={() => setShowPreview(false)}
className="p-1.5 text-muted-foreground hover:text-foreground rounded-lg"
>
<XCircle className="w-4 h-4" />
</button>
</div>
</div>
<div className="w-full aspect-[430/932] bg-white">
<iframe ref={iframeRef} src={previewPath} className="w-full h-full" title="Page Preview" />
</div>
</div>
</div>
)}
</main>
)
}

View File

@@ -0,0 +1,136 @@
"use client"
import { useState } from "react"
import { useRouter } from "next/navigation"
import Link from "next/link"
import { ChevronLeft, Phone, Hash } from "lucide-react"
export default function ForgotPasswordPage() {
const router = useRouter()
const [phone, setPhone] = useState("")
const [newPassword, setNewPassword] = useState("")
const [confirmPassword, setConfirmPassword] = useState("")
const [error, setError] = useState("")
const [success, setSuccess] = useState(false)
const [loading, setLoading] = useState(false)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError("")
setLoading(true)
try {
if (!phone.trim()) {
setError("请输入手机号")
return
}
if (!newPassword.trim()) {
setError("请输入新密码")
return
}
if (newPassword.trim().length < 6) {
setError("密码至少 6 位")
return
}
if (newPassword !== confirmPassword) {
setError("两次输入的密码不一致")
return
}
const res = await fetch("/api/auth/reset-password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ phone: phone.trim(), newPassword: newPassword.trim() }),
})
const data = await res.json()
if (data.success) {
setSuccess(true)
setTimeout(() => router.push("/view/login"), 2000)
} else {
setError(data.error || "重置失败")
}
} finally {
setLoading(false)
}
}
if (success) {
return (
<div className="min-h-screen bg-black text-white flex flex-col items-center justify-center px-6">
<p className="text-[#30d158] text-lg mb-4"></p>
<p className="text-gray-500 text-sm">使...</p>
</div>
)
}
return (
<div className="min-h-screen bg-black text-white flex flex-col">
<header className="flex items-center px-4 py-3">
<Link
href="/view/login"
className="w-9 h-9 rounded-full bg-[#1c1c1e] flex items-center justify-center"
>
<ChevronLeft className="w-5 h-5 text-gray-400" />
</Link>
<h1 className="flex-1 text-center text-lg font-semibold"></h1>
<div className="w-9" />
</header>
<main className="flex-1 px-6 pt-8">
<p className="text-gray-500 text-sm mb-6">
使使
</p>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="relative">
<Phone className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500" />
<input
type="tel"
value={phone}
onChange={(e) => setPhone(e.target.value)}
placeholder="手机号"
className="w-full pl-12 pr-4 py-3.5 bg-[#1c1c1e] rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[#30d158]/50"
/>
</div>
<div className="relative">
<Hash className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500" />
<input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder="新密码(至少 6 位)"
className="w-full pl-12 pr-4 py-3.5 bg-[#1c1c1e] rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[#30d158]/50"
/>
</div>
<div className="relative">
<Hash className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500" />
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="再次输入新密码"
className="w-full pl-12 pr-4 py-3.5 bg-[#1c1c1e] rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[#30d158]/50"
/>
</div>
{error && <p className="text-red-500 text-sm">{error}</p>}
<button
type="submit"
disabled={loading || !phone || !newPassword || !confirmPassword}
className="w-full py-3.5 bg-[#30d158] text-white font-medium rounded-xl active:scale-[0.98] transition-transform disabled:opacity-50"
>
{loading ? "提交中..." : "重置密码"}
</button>
</form>
<p className="text-gray-500 text-xs mt-6 text-center">
使
</p>
</main>
</div>
)
}

183
app/view/login/page.tsx Normal file
View File

@@ -0,0 +1,183 @@
"use client"
import { useState } from "react"
import { useRouter } from "next/navigation"
import Link from "next/link"
import { useStore } from "@/lib/store"
import { ChevronLeft, Phone, User, Hash } from "lucide-react"
export default function LoginPage() {
const router = useRouter()
const { login, register } = useStore()
const [mode, setMode] = useState<"login" | "register">("login")
const [phone, setPhone] = useState("")
const [code, setCode] = useState("")
const [nickname, setNickname] = useState("")
const [referralCode, setReferralCode] = useState("")
const [error, setError] = useState("")
const [loading, setLoading] = useState(false)
const handleSubmit = async () => {
setError("")
setLoading(true)
try {
// 管理员登录(使用 code 作为密码,调用后台 API 并写 Cookie
if (phone.toLowerCase() === "admin") {
try {
const res = await fetch("/api/admin", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: phone, password: code }),
credentials: "include",
})
const data = await res.json()
if (res.ok && data.success) {
router.push("/admin")
return
}
} catch {
// fallthrough to error
}
setError("管理员密码错误")
return
}
if (mode === "login") {
if (!code.trim()) {
setError("请输入密码")
return
}
const success = await login(phone, code)
if (success) {
router.push("/view")
} else {
setError("密码错误或用户不存在")
}
} else {
if (!nickname.trim()) {
setError("请输入昵称")
return
}
if (!code.trim()) {
setError("请设置密码(至少 6 位)")
return
}
if (code.trim().length < 6) {
setError("密码至少 6 位")
return
}
const success = await register(phone, nickname, code, referralCode || undefined)
if (success) {
router.push("/view")
} else {
setError("该手机号已注册")
}
}
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen bg-black text-white flex flex-col">
{/* 顶部导航 */}
<header className="flex items-center px-4 py-3">
<button
onClick={() => router.back()}
className="w-9 h-9 rounded-full bg-[#1c1c1e] flex items-center justify-center"
>
<ChevronLeft className="w-5 h-5 text-gray-400" />
</button>
</header>
{/* 主内容 */}
<main className="flex-1 px-6 pt-8">
<h1 className="text-2xl font-bold mb-2">{mode === "login" ? "登录" : "注册"}</h1>
<p className="text-gray-500 text-sm mb-8">
{mode === "login" ? "登录后查看购买记录和收益" : "注册后开始阅读真实商业故事"}
</p>
<div className="space-y-4">
{/* 手机号 */}
<div className="relative">
<Phone className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500" />
<input
type="tel"
value={phone}
onChange={(e) => setPhone(e.target.value)}
placeholder="手机号"
className="w-full pl-12 pr-4 py-3.5 bg-[#1c1c1e] rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[#30d158]/50"
/>
</div>
{/* 昵称(注册时显示) */}
{mode === "register" && (
<div className="relative">
<User className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500" />
<input
type="text"
value={nickname}
onChange={(e) => setNickname(e.target.value)}
placeholder="昵称"
className="w-full pl-12 pr-4 py-3.5 bg-[#1c1c1e] rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[#30d158]/50"
/>
</div>
)}
{/* 密码 */}
<div className="relative">
<Hash className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500" />
<input
type="password"
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder={mode === "login" ? "密码" : "设置密码(至少 6 位)"}
className="w-full pl-12 pr-4 py-3.5 bg-[#1c1c1e] rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[#30d158]/50"
/>
</div>
{/* 邀请码(注册时显示) */}
{mode === "register" && (
<div className="relative">
<Hash className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500" />
<input
type="text"
value={referralCode}
onChange={(e) => setReferralCode(e.target.value)}
placeholder="邀请码(选填)"
className="w-full pl-12 pr-4 py-3.5 bg-[#1c1c1e] rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[#30d158]/50"
/>
</div>
)}
{/* 错误提示 */}
{error && <p className="text-red-500 text-sm">{error}</p>}
{/* 提交按钮 */}
<button
onClick={handleSubmit}
disabled={loading || !phone}
className="w-full py-3.5 bg-[#30d158] text-white font-medium rounded-xl active:scale-[0.98] transition-transform disabled:opacity-50"
>
{loading ? "处理中..." : mode === "login" ? "登录" : "注册"}
</button>
{/* 忘记密码 / 切换模式 */}
<div className="text-center space-y-2">
{mode === "login" && (
<div>
<Link href="/view/login/forgot" className="text-[#30d158] text-sm">
</Link>
</div>
)}
<button onClick={() => setMode(mode === "login" ? "register" : "login")} className="text-[#30d158] text-sm block mx-auto">
{mode === "login" ? "没有账号?去注册" : "已有账号?去登录"}
</button>
</div>
</div>
</main>
</div>
)
}

849
app/view/match/page.tsx Normal file
View File

@@ -0,0 +1,849 @@
"use client"
import { useState, useEffect } from "react"
import { motion, AnimatePresence } from "framer-motion"
import { Users, X, CheckCircle, Loader2, Lock, Zap } from "lucide-react"
import { useRouter } from "next/navigation"
import { useStore } from "@/lib/store"
interface MatchUser {
id: string
nickname: string
avatar: string
tags: string[]
matchScore: number
concept: string
wechat: string
commonInterests: Array<{ icon: string; text: string }>
}
const matchTypes = [
{ id: "partner", label: "创业合伙", matchLabel: "创业伙伴", icon: "⭐", color: "#00E5FF", matchFromDB: true, showJoinAfterMatch: false },
{ id: "investor", label: "资源对接", matchLabel: "资源对接", icon: "👥", color: "#7B61FF", matchFromDB: false, showJoinAfterMatch: true },
{ id: "mentor", label: "导师顾问", matchLabel: "商业顾问", icon: "❤️", color: "#E91E63", matchFromDB: false, showJoinAfterMatch: true },
{ id: "team", label: "团队招募", matchLabel: "加入项目", icon: "🎮", color: "#4CAF50", matchFromDB: false, showJoinAfterMatch: true },
]
const FREE_MATCH_LIMIT = 1 // 每日免费匹配次数改为1次
const MATCH_UNLOCK_PRICE = 1 // 每次解锁需要购买1个小节
// 获取本地存储的联系方式
const getStoredContact = (): { phone: string; wechat: string } => {
if (typeof window !== "undefined") {
return {
phone: localStorage.getItem("user_phone") || "",
wechat: localStorage.getItem("user_wechat") || "",
}
}
return { phone: "", wechat: "" }
}
// 获取今日匹配次数
const getTodayMatchCount = (): number => {
if (typeof window !== "undefined") {
const today = new Date().toISOString().split('T')[0]
const stored = localStorage.getItem("match_count_data")
if (stored) {
const data = JSON.parse(stored)
if (data.date === today) {
return data.count
}
}
}
return 0
}
// 保存今日匹配次数
const saveTodayMatchCount = (count: number) => {
if (typeof window !== "undefined") {
const today = new Date().toISOString().split('T')[0]
localStorage.setItem("match_count_data", JSON.stringify({ date: today, count }))
}
}
// 保存联系方式到本地存储
const saveContact = (phone: string, wechat: string) => {
if (typeof window !== "undefined") {
if (phone) localStorage.setItem("user_phone", phone)
if (wechat) localStorage.setItem("user_wechat", wechat)
}
}
export default function MatchPage() {
const [mounted, setMounted] = useState(false)
const [isMatching, setIsMatching] = useState(false)
const [currentMatch, setCurrentMatch] = useState<MatchUser | null>(null)
const [matchAttempts, setMatchAttempts] = useState(0)
const [selectedType, setSelectedType] = useState("partner")
const [todayMatchCount, setTodayMatchCount] = useState(0)
const router = useRouter()
const { user, isLoggedIn, purchaseSection } = useStore()
const [showJoinModal, setShowJoinModal] = useState(false)
const [showUnlockModal, setShowUnlockModal] = useState(false)
const [joinType, setJoinType] = useState<string | null>(null)
const [phoneNumber, setPhoneNumber] = useState("")
const [wechatId, setWechatId] = useState("")
const [contactType, setContactType] = useState<"phone" | "wechat">("phone")
const [isJoining, setIsJoining] = useState(false)
const [joinSuccess, setJoinSuccess] = useState(false)
const [joinError, setJoinError] = useState("")
const [isUnlocking, setIsUnlocking] = useState(false)
// 检查用户是否有购买权限(购买过任意内容)
const hasPurchased = user?.hasFullBook || (user?.purchasedSections && user.purchasedSections.length > 0)
// 总共获得的匹配次数 = 每日免费(1) + 已购小节数量
// 如果购买了全书,则拥有无限匹配机会
const totalMatchesAllowed = user?.hasFullBook ? 999999 : FREE_MATCH_LIMIT + (user?.purchasedSections?.length || 0)
// 剩余可用次数
const matchesRemaining = user?.hasFullBook ? 999999 : Math.max(0, totalMatchesAllowed - todayMatchCount)
// 是否需要付费(总次数用完)
const needPayToMatch = !user?.hasFullBook && matchesRemaining <= 0
// 初始化
useEffect(() => {
setMounted(true)
const storedContact = getStoredContact()
if (storedContact.phone) {
setPhoneNumber(storedContact.phone)
}
if (storedContact.wechat) {
setWechatId(storedContact.wechat)
}
if (user?.phone) {
setPhoneNumber(user.phone)
}
// 读取今日匹配次数
setTodayMatchCount(getTodayMatchCount())
}, [user])
// 处理函数定义(必须在所有 hooks 之后)
const handleJoinClick = (typeId: string) => {
setJoinType(typeId)
setShowJoinModal(true)
setJoinSuccess(false)
setJoinError("")
}
const handleJoinSubmit = async () => {
const contact = contactType === "phone" ? phoneNumber : wechatId
if (contactType === "phone" && (!phoneNumber || phoneNumber.length !== 11)) {
setJoinError("请输入正确的11位手机号")
return
}
if (contactType === "wechat" && (!wechatId || wechatId.length < 6)) {
setJoinError("请输入正确的微信号至少6位")
return
}
setIsJoining(true)
setJoinError("")
try {
const response = await fetch("/api/ckb/join", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
type: joinType,
phone: contactType === "phone" ? phoneNumber : "",
wechat: contactType === "wechat" ? wechatId : "",
userId: user?.id,
}),
})
const result = await response.json()
if (result.success) {
saveContact(phoneNumber, wechatId)
setJoinSuccess(true)
setTimeout(() => {
setShowJoinModal(false)
setJoinSuccess(false)
}, 2000)
} else {
setJoinError(result.message || "加入失败,请稍后重试")
}
} catch (error) {
setJoinError("网络错误,请检查网络后重试")
} finally {
setIsJoining(false)
}
}
// 购买解锁匹配次数
const handleUnlockMatch = async () => {
if (!isLoggedIn) {
alert("请先登录")
return
}
setIsUnlocking(true)
try {
// 模拟购买过程实际应该调用支付API
// 这里简化为直接购买成功
await new Promise((resolve) => setTimeout(resolve, 1500))
// 购买成功后重置今日匹配次数增加3次
const newCount = Math.max(0, todayMatchCount - 3)
saveTodayMatchCount(newCount)
setTodayMatchCount(newCount)
setShowUnlockModal(false)
alert("解锁成功已获得3次匹配机会")
} catch (error) {
alert("解锁失败,请重试")
} finally {
setIsUnlocking(false)
}
}
// 上报匹配行为到CKB
const reportMatchToCKB = async (matchedUser: MatchUser) => {
try {
await fetch("/api/ckb/match", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
matchType: selectedType,
phone: phoneNumber || user?.phone || "",
wechat: wechatId || user?.wechat || "",
userId: user?.id || "",
nickname: user?.nickname || "",
matchedUser: {
id: matchedUser.id,
nickname: matchedUser.nickname,
matchScore: matchedUser.matchScore,
},
}),
})
} catch (error) {
console.error("上报匹配失败:", error)
}
}
const startMatch = () => {
// 检查是否有购买权限
if (!hasPurchased) {
return
}
// 检查是否需要付费
if (needPayToMatch) {
setShowUnlockModal(true)
return
}
setIsMatching(true)
setMatchAttempts(0)
setCurrentMatch(null)
const interval = setInterval(() => {
setMatchAttempts((prev) => prev + 1)
}, 1000)
setTimeout(
() => {
clearInterval(interval)
setIsMatching(false)
const matchedUser = getMockMatch()
setCurrentMatch(matchedUser)
// 增加今日匹配次数
const newCount = todayMatchCount + 1
setTodayMatchCount(newCount)
saveTodayMatchCount(newCount)
// 上报匹配行为
reportMatchToCKB(matchedUser)
// 如果是需要弹出加入弹窗的类型,自动弹出
const currentType = matchTypes.find(t => t.id === selectedType)
if (currentType?.showJoinAfterMatch) {
setJoinType(selectedType)
setShowJoinModal(true)
setJoinSuccess(false)
setJoinError("")
}
},
Math.random() * 3000 + 3000,
)
}
const getMockMatch = (): MatchUser => {
const nicknames = ["创业先锋", "资源整合者", "私域专家", "商业导师", "连续创业者"]
const randomIndex = Math.floor(Math.random() * nicknames.length)
const concepts = [
"专注私域流量运营5年帮助100+品牌实现从0到1的增长。",
"连续创业者,擅长商业模式设计和资源整合。",
"在Soul分享真实创业故事希望找到志同道合的合作伙伴。",
]
const wechats = ["soul_partner_1", "soul_business_2024", "soul_startup_fan"]
return {
id: `user_${Date.now()}`,
nickname: nicknames[randomIndex],
avatar: `https://picsum.photos/200/200?random=${randomIndex}`,
tags: ["创业者", "私域运营", matchTypes.find((t) => t.id === selectedType)?.label || ""],
matchScore: Math.floor(Math.random() * 20) + 80,
concept: concepts[randomIndex % concepts.length],
wechat: wechats[randomIndex % wechats.length],
commonInterests: [
{ icon: "📚", text: "都在读《创业实验》" },
{ icon: "💼", text: "对私域运营感兴趣" },
{ icon: "🎯", text: "相似的创业方向" },
],
}
}
const nextMatch = () => {
// 检查是否需要付费
if (needPayToMatch) {
setShowUnlockModal(true)
return
}
setCurrentMatch(null)
setTimeout(() => startMatch(), 500)
}
const handleAddWechat = () => {
if (!currentMatch) return
navigator.clipboard
.writeText(currentMatch.wechat)
.then(() => {
alert(`微信号已复制:${currentMatch.wechat}\n\n请打开微信添加好友备注"创业合作"即可。`)
})
.catch(() => {
alert(`微信号:${currentMatch.wechat}\n\n请手动复制并添加好友。`)
})
}
const currentType = matchTypes.find((t) => t.id === selectedType)
const currentTypeLabel = currentType?.label || "创业合伙"
const currentMatchLabel = currentType?.matchLabel || "创业伙伴"
const joinTypeLabel = matchTypes.find((t) => t.id === joinType)?.matchLabel || ""
// 等待挂载完成(必须在所有 hooks 和函数定义之后)
if (!mounted) return null
return (
<div className="min-h-screen bg-black pb-24">
<div className="flex items-center justify-between px-6 pt-6 pb-4">
<h1 className="text-2xl font-bold text-white"></h1>
<button className="w-10 h-10 rounded-full bg-[#1c1c1e] flex items-center justify-center">
<svg className="w-5 h-5 text-white/60" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"
/>
</svg>
</button>
</div>
{/* 今日匹配次数显示 - 仅在总次数用完时显示 */}
{hasPurchased && (
<div className="px-6 mb-4">
<div className={`flex items-center justify-between p-3 rounded-xl bg-[#1c1c1e] border ${matchesRemaining <= 0 && !user?.hasFullBook ? 'border-[#FFD700]/20' : 'border-white/5'}`}>
<div className="flex items-center gap-2">
<Zap className={`w-5 h-5 ${matchesRemaining <= 0 && !user?.hasFullBook ? 'text-[#FFD700]' : 'text-[#00E5FF]'}`} />
<span className="text-white/70 text-sm">
{user?.hasFullBook ? "无限匹配机会" : matchesRemaining <= 0 ? "今日匹配机会已用完" : "剩余匹配机会"}
</span>
</div>
<div className="flex items-center gap-2">
<span className={`text-lg font-bold ${matchesRemaining > 0 ? 'text-[#00E5FF]' : 'text-red-400'}`}>
{user?.hasFullBook ? "无限" : `${matchesRemaining}/${totalMatchesAllowed}`}
</span>
{matchesRemaining <= 0 && !user?.hasFullBook && (
<button
onClick={() => router.push('/view/chapters')}
className="px-3 py-1.5 rounded-full bg-[#FFD700]/20 text-[#FFD700] text-xs font-medium"
>
+1
</button>
)}
</div>
</div>
</div>
)}
<AnimatePresence mode="wait">
{!isMatching && !currentMatch && (
<motion.div
key="idle"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="flex flex-col items-center px-6"
>
{/* 中央匹配圆环 */}
<motion.div
onClick={hasPurchased ? startMatch : undefined}
className={`relative w-[280px] h-[280px] mb-8 ${hasPurchased ? 'cursor-pointer' : 'cursor-not-allowed'}`}
whileTap={hasPurchased ? { scale: 0.95 } : undefined}
>
{/* 外层光环 */}
<motion.div
className="absolute inset-[-30px] rounded-full"
style={{
background: hasPurchased
? "radial-gradient(circle, transparent 50%, rgba(0, 229, 255, 0.1) 70%, transparent 100%)"
: "radial-gradient(circle, transparent 50%, rgba(100, 100, 100, 0.1) 70%, transparent 100%)",
}}
animate={{
scale: [1, 1.1, 1],
opacity: [0.5, 0.8, 0.5],
}}
transition={{
duration: 2,
repeat: Number.POSITIVE_INFINITY,
ease: "easeInOut",
}}
/>
{/* 中间光环 */}
<motion.div
className={`absolute inset-[-15px] rounded-full border-2 ${hasPurchased ? 'border-[#00E5FF]/30' : 'border-gray-600/30'}`}
animate={{
scale: [1, 1.05, 1],
opacity: [0.3, 0.6, 0.3],
}}
transition={{
duration: 1.5,
repeat: Number.POSITIVE_INFINITY,
ease: "easeInOut",
}}
/>
{/* 内层渐变球 */}
<motion.div
className="absolute inset-0 rounded-full flex flex-col items-center justify-center overflow-hidden"
style={{
background: hasPurchased
? "linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)"
: "linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 50%, #1a1a1a 100%)",
boxShadow: hasPurchased
? "0 0 60px rgba(0, 229, 255, 0.3), inset 0 0 60px rgba(123, 97, 255, 0.2)"
: "0 0 30px rgba(100, 100, 100, 0.2)",
}}
animate={{
y: [0, -5, 0],
}}
transition={{
duration: 3,
repeat: Number.POSITIVE_INFINITY,
ease: "easeInOut",
}}
>
{/* 内部渐变光效 */}
<div
className="absolute inset-0 rounded-full"
style={{
background: hasPurchased
? "radial-gradient(circle at 30% 30%, rgba(123, 97, 255, 0.4) 0%, transparent 50%), radial-gradient(circle at 70% 70%, rgba(233, 30, 99, 0.3) 0%, transparent 50%)"
: "radial-gradient(circle at 30% 30%, rgba(100, 100, 100, 0.2) 0%, transparent 50%)",
}}
/>
{/* 中心图标 */}
{hasPurchased ? (
needPayToMatch ? (
<>
<Zap className="w-12 h-12 text-[#FFD700] mb-3 relative z-10" />
<div className="text-xl font-bold text-white mb-1 relative z-10"></div>
<div className="text-sm text-white/60 relative z-10"></div>
</>
) : (
<>
<Users className="w-12 h-12 text-white/90 mb-3 relative z-10" />
<div className="text-xl font-bold text-white mb-1 relative z-10"></div>
<div className="text-sm text-white/60 relative z-10">{currentMatchLabel}</div>
</>
)
) : (
<>
<Lock className="w-12 h-12 text-gray-500 mb-3 relative z-10" />
<div className="text-xl font-bold text-gray-400 mb-1 relative z-10"></div>
<div className="text-sm text-gray-500 relative z-10">9.9使</div>
</>
)}
</motion.div>
</motion.div>
{/* 当前模式显示 */}
<p className="text-white/50 text-sm mb-4">
: <span className={hasPurchased ? "text-[#00E5FF]" : "text-gray-500"}>{currentTypeLabel}</span>
</p>
{/* 购买提示 */}
{!hasPurchased && (
<div className="w-full mb-6 p-4 rounded-xl bg-gradient-to-r from-[#00E5FF]/10 to-transparent border border-[#00E5FF]/20">
<div className="flex items-center justify-between">
<div>
<p className="text-white font-medium"></p>
<p className="text-gray-400 text-sm mt-1">9.93</p>
</div>
<button
onClick={() => router.push('/view/chapters')}
className="px-4 py-2 rounded-lg bg-[#00E5FF] text-black text-sm font-medium"
>
</button>
</div>
</div>
)}
{/* 分隔线 */}
<div className="w-full h-px bg-white/10 mb-6" />
{/* 选择匹配类型 */}
<p className="text-white/40 text-sm mb-4"></p>
<div className="grid grid-cols-4 gap-3 w-full">
{matchTypes.map((type) => (
<button
key={type.id}
onClick={() => setSelectedType(type.id)}
className={`p-4 rounded-xl flex flex-col items-center gap-2 transition-all ${
selectedType === type.id
? "bg-[#00E5FF]/10 border border-[#00E5FF]/50"
: "bg-[#1c1c1e] border border-transparent"
}`}
>
<span className="text-2xl">{type.icon}</span>
<span className={`text-xs ${selectedType === type.id ? "text-[#00E5FF]" : "text-white/60"}`}>
{type.label}
</span>
</button>
))}
</div>
</motion.div>
)}
{isMatching && (
<motion.div
key="matching"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="text-center px-6"
>
{/* 匹配动画 */}
<div className="relative w-[200px] h-[200px] mx-auto mb-8">
<motion.div
className="absolute inset-0 rounded-full bg-gradient-to-br from-[#00E5FF] via-[#7B61FF] to-[#E91E63]"
animate={{ rotate: 360 }}
transition={{ duration: 3, repeat: Number.POSITIVE_INFINITY, ease: "linear" }}
/>
<div className="absolute inset-2 rounded-full bg-black flex items-center justify-center">
<motion.div
animate={{ scale: [1, 1.2, 1] }}
transition={{ duration: 1, repeat: Number.POSITIVE_INFINITY }}
>
<Users className="w-12 h-12 text-[#00E5FF]" />
</motion.div>
</div>
{/* 扩散波纹 */}
{[1, 2, 3].map((ring) => (
<motion.div
key={ring}
className="absolute inset-0 rounded-full border-2 border-[#00E5FF]/30"
animate={{
scale: [1, 2],
opacity: [0.6, 0],
}}
transition={{
duration: 2,
repeat: Number.POSITIVE_INFINITY,
delay: ring * 0.5,
}}
/>
))}
</div>
<h2 className="text-xl font-semibold mb-2 text-white">{currentMatchLabel}...</h2>
<p className="text-white/50 mb-8"> {matchAttempts} </p>
<button
onClick={() => setIsMatching(false)}
className="px-8 py-3 rounded-full bg-[#1c1c1e] text-white border border-white/10"
>
</button>
</motion.div>
)}
{currentMatch && !isMatching && (
<motion.div
key="matched"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="px-6"
>
{/* 成功动画 */}
<motion.div
className="text-center mb-6"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: "spring", bounce: 0.5 }}
>
<span className="text-6xl"></span>
</motion.div>
{/* 用户卡片 */}
<div className="bg-[#1c1c1e] rounded-2xl p-5 mb-4 border border-white/5">
<div className="flex items-center gap-4 mb-4">
<img
src={currentMatch.avatar || "/placeholder.svg"}
alt={currentMatch.nickname}
className="w-16 h-16 rounded-full border-2 border-[#00E5FF]"
/>
<div className="flex-1">
<h3 className="text-lg font-semibold text-white mb-2">{currentMatch.nickname}</h3>
<div className="flex flex-wrap gap-1">
{currentMatch.tags.map((tag) => (
<span key={tag} className="px-2 py-0.5 rounded text-xs bg-[#00E5FF]/20 text-[#00E5FF]">
{tag}
</span>
))}
</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-[#00E5FF]">{currentMatch.matchScore}%</div>
<div className="text-xs text-white/50"></div>
</div>
</div>
{/* 共同兴趣 */}
<div className="pt-4 border-t border-white/10 mb-4">
<h4 className="text-sm text-white/60 mb-2"></h4>
<div className="space-y-2">
{currentMatch.commonInterests.map((interest, i) => (
<div key={i} className="flex items-center gap-2 text-sm text-white/80">
<span>{interest.icon}</span>
<span>{interest.text}</span>
</div>
))}
</div>
</div>
{/* 核心理念 */}
<div className="pt-4 border-t border-white/10">
<h4 className="text-sm text-white/60 mb-2"></h4>
<p className="text-sm text-white/70">{currentMatch.concept}</p>
</div>
</div>
{/* 操作按钮 */}
<div className="space-y-3">
<button onClick={handleAddWechat} className="w-full py-4 rounded-xl bg-[#00E5FF] text-black font-medium">
</button>
<button
onClick={nextMatch}
className="w-full py-4 rounded-xl bg-[#1c1c1e] text-white border border-white/10 flex items-center justify-center gap-2"
>
{matchesRemaining <= 0 && (
<span className="text-xs px-2 py-0.5 rounded-full bg-[#FFD700]/20 text-[#FFD700]"></span>
)}
</button>
</div>
</motion.div>
)}
</AnimatePresence>
{/* 解锁匹配次数弹窗 */}
<AnimatePresence>
{showUnlockModal && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center px-6"
onClick={() => !isUnlocking && setShowUnlockModal(false)}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
onClick={(e) => e.stopPropagation()}
className="bg-[#1c1c1e] rounded-2xl w-full max-w-sm overflow-hidden"
>
<div className="p-6 text-center">
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-[#FFD700]/20 flex items-center justify-center">
<Zap className="w-8 h-8 text-[#FFD700]" />
</div>
<h3 className="text-xl font-bold text-white mb-2"></h3>
<p className="text-white/60 text-sm mb-6">
1
</p>
<div className="bg-black/30 rounded-xl p-4 mb-6">
<div className="flex items-center justify-between mb-2">
<span className="text-white/60"></span>
<span className="text-white font-medium"></span>
</div>
<div className="flex items-center justify-between">
<span className="text-white/60"></span>
<span className="text-[#00E5FF] font-medium">+1</span>
</div>
</div>
<div className="space-y-3">
<button
onClick={() => {
setShowUnlockModal(false)
router.push('/view/chapters')
}}
className="w-full py-3 rounded-xl bg-[#FFD700] text-black font-medium"
>
(¥1/)
</button>
<button
onClick={() => setShowUnlockModal(false)}
className="w-full py-3 rounded-xl bg-white/5 text-white/60"
>
</button>
</div>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
{/* 加入类型弹窗 */}
<AnimatePresence>
{showJoinModal && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center px-6"
onClick={() => !isJoining && setShowJoinModal(false)}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
onClick={(e) => e.stopPropagation()}
className="bg-[#1c1c1e] rounded-2xl w-full max-w-sm overflow-hidden"
>
{/* 弹窗头部 */}
<div className="flex items-center justify-between p-4 border-b border-white/10">
<h3 className="text-lg font-semibold text-white">{joinTypeLabel}</h3>
<button
onClick={() => !isJoining && setShowJoinModal(false)}
className="w-8 h-8 rounded-full bg-white/10 flex items-center justify-center"
>
<X className="w-4 h-4 text-white/60" />
</button>
</div>
{/* 弹窗内容 */}
<div className="p-5">
{joinSuccess ? (
<motion.div initial={{ scale: 0.8 }} animate={{ scale: 1 }} className="text-center py-8">
<CheckCircle className="w-16 h-16 text-[#00E5FF] mx-auto mb-4" />
<p className="text-white text-lg font-medium mb-2">!</p>
<p className="text-white/60 text-sm"></p>
</motion.div>
) : (
<>
<p className="text-white/60 text-sm mb-4">
{user?.phone ? "已检测到您的绑定信息,可直接提交或修改" : "请填写您的联系方式以便我们联系您"}
</p>
{/* 联系方式类型切换 */}
<div className="flex gap-2 mb-4">
<button
onClick={() => setContactType("phone")}
className={`flex-1 py-2 px-4 rounded-lg text-sm font-medium transition-colors ${
contactType === "phone"
? "bg-[#00E5FF]/20 text-[#00E5FF] border border-[#00E5FF]/30"
: "bg-white/5 text-gray-400 border border-white/10"
}`}
>
</button>
<button
onClick={() => setContactType("wechat")}
className={`flex-1 py-2 px-4 rounded-lg text-sm font-medium transition-colors ${
contactType === "wechat"
? "bg-[#07C160]/20 text-[#07C160] border border-[#07C160]/30"
: "bg-white/5 text-gray-400 border border-white/10"
}`}
>
</button>
</div>
{/* 联系方式输入 */}
<div className="mb-4">
<label className="block text-white/40 text-xs mb-2">
{contactType === "phone" ? "手机号" : "微信号"}
</label>
{contactType === "phone" ? (
<input
type="tel"
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value.replace(/\D/g, "").slice(0, 11))}
placeholder="请输入11位手机号"
className="w-full px-4 py-3 rounded-xl bg-black/30 border border-white/10 text-white placeholder-white/30 focus:outline-none focus:border-[#00E5FF]/50"
disabled={isJoining}
/>
) : (
<input
type="text"
value={wechatId}
onChange={(e) => setWechatId(e.target.value)}
placeholder="请输入微信号"
className="w-full px-4 py-3 rounded-xl bg-black/30 border border-white/10 text-white placeholder-white/30 focus:outline-none focus:border-[#07C160]/50"
disabled={isJoining}
/>
)}
</div>
{/* 错误提示 */}
{joinError && <p className="text-red-400 text-sm mb-4">{joinError}</p>}
{/* 提交按钮 */}
<button
onClick={handleJoinSubmit}
disabled={isJoining || (contactType === "phone" ? !phoneNumber : !wechatId)}
className="w-full py-3 rounded-xl bg-[#00E5FF] text-black font-medium flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isJoining ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
...
</>
) : (
"确认加入"
)}
</button>
<p className="text-white/30 text-xs text-center mt-3"></p>
</>
)}
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
)
}

View File

@@ -0,0 +1,161 @@
"use client"
import { useState, useEffect } from "react"
import { useRouter, useParams } from "next/navigation"
import { ChevronLeft } from "lucide-react"
import { useStore } from "@/lib/store"
export default function EditAddressPage() {
const router = useRouter()
const params = useParams()
const id = params?.id as string
const { user } = useStore()
const [loading, setLoading] = useState(false)
const [fetching, setFetching] = useState(true)
const [name, setName] = useState("")
const [phone, setPhone] = useState("")
const [province, setProvince] = useState("")
const [city, setCity] = useState("")
const [district, setDistrict] = useState("")
const [detail, setDetail] = useState("")
const [isDefault, setIsDefault] = useState(false)
useEffect(() => {
if (!id || !user?.id) {
setFetching(false)
return
}
fetch(`/api/user/addresses/${id}`)
.then((res) => res.json())
.then((data) => {
if (data.success && data.item) {
const a = data.item
setName(a.name || "")
setPhone(a.phone || "")
setProvince(a.province || "")
setCity(a.city || "")
setDistrict(a.district || "")
setDetail(a.detail || "")
setIsDefault(!!a.isDefault)
}
})
.finally(() => setFetching(false))
}, [id, user?.id])
if (!user?.id) {
return (
<div className="min-h-screen bg-black text-white flex items-center justify-center">
<p className="text-white/60"></p>
</div>
)
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!name.trim()) {
alert("请输入收货人姓名")
return
}
if (!/^1[3-9]\d{9}$/.test(phone)) {
alert("请输入正确的手机号")
return
}
if (!detail.trim()) {
alert("请输入详细地址")
return
}
setLoading(true)
try {
const res = await fetch(`/api/user/addresses/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: name.trim(),
phone: phone.trim(),
province: (province ?? "").trim(),
city: (city ?? "").trim(),
district: (district ?? "").trim(),
detail: detail.trim(),
isDefault,
}),
})
const data = await res.json()
if (data.success) {
router.push("/view/my/addresses")
} else {
alert(data.message || "保存失败")
}
} catch {
alert("保存失败")
} finally {
setLoading(false)
}
}
if (fetching) {
return (
<div className="min-h-screen bg-black text-white flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-[#00CED1]" />
</div>
)
}
return (
<div className="min-h-screen bg-black text-white pb-24">
<header className="sticky top-0 z-40 bg-black/90 backdrop-blur-xl border-b border-white/5">
<div className="px-4 py-3 flex items-center">
<button onClick={() => router.back()} className="w-8 h-8 rounded-full bg-white/10 flex items-center justify-center">
<ChevronLeft className="w-5 h-5 text-white" />
</button>
<h1 className="flex-1 text-center text-lg font-semibold text-white"></h1>
<div className="w-8" />
</div>
</header>
<form onSubmit={handleSubmit} className="px-4 py-4 space-y-4">
<div className="rounded-2xl bg-[#1c1c1e] border border-white/5 overflow-hidden">
<div className="flex items-center justify-between p-4 border-b border-white/5">
<label className="text-white text-sm w-24"></label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="请输入收货人姓名"
className="flex-1 bg-transparent text-white text-sm text-right placeholder-white/30 outline-none"
/>
</div>
<div className="flex items-center justify-between p-4 border-b border-white/5">
<label className="text-white text-sm w-24"></label>
<input
type="tel"
maxLength={11}
value={phone}
onChange={(e) => setPhone(e.target.value.replace(/\D/g, ""))}
placeholder="请输入手机号"
className="flex-1 bg-transparent text-white text-sm text-right placeholder-white/30 outline-none"
/>
</div>
<div className="flex items-center justify-between p-4 border-b border-white/5">
<label className="text-white text-sm w-24"></label>
<div className="flex-1 flex gap-2 justify-end">
<input type="text" value={province} onChange={(e) => setProvince(e.target.value)} placeholder="省" className="flex-1 max-w-24 bg-transparent text-white text-sm text-right placeholder-white/30 outline-none" />
<input type="text" value={city} onChange={(e) => setCity(e.target.value)} placeholder="市" className="flex-1 max-w-24 bg-transparent text-white text-sm text-right placeholder-white/30 outline-none" />
<input type="text" value={district} onChange={(e) => setDistrict(e.target.value)} placeholder="区" className="flex-1 max-w-24 bg-transparent text-white text-sm text-right placeholder-white/30 outline-none" />
</div>
</div>
<div className="flex items-start justify-between p-4">
<label className="text-white text-sm w-24 pt-2"></label>
<textarea value={detail} onChange={(e) => setDetail(e.target.value)} placeholder="街道、楼栋、门牌号等" rows={3} className="flex-1 bg-transparent text-white text-sm text-right placeholder-white/30 outline-none resize-none" />
</div>
<div className="flex items-center justify-between p-4 border-t border-white/5">
<span className="text-white text-sm"></span>
<input type="checkbox" checked={isDefault} onChange={(e) => setIsDefault(e.target.checked)} className="w-5 h-5 rounded accent-[#00CED1]" />
</div>
</div>
<button type="submit" disabled={loading} className="w-full py-3 rounded-xl bg-[#00CED1] text-black font-medium disabled:opacity-50">
{loading ? "保存中..." : "保存"}
</button>
</form>
</div>
)
}

View File

@@ -0,0 +1,163 @@
"use client"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { ChevronLeft } from "lucide-react"
import { useStore } from "@/lib/store"
export default function NewAddressPage() {
const router = useRouter()
const { user } = useStore()
const [loading, setLoading] = useState(false)
const [name, setName] = useState("")
const [phone, setPhone] = useState("")
const [province, setProvince] = useState("")
const [city, setCity] = useState("")
const [district, setDistrict] = useState("")
const [detail, setDetail] = useState("")
const [isDefault, setIsDefault] = useState(false)
if (!user?.id) {
return (
<div className="min-h-screen bg-black text-white flex items-center justify-center">
<p className="text-white/60"></p>
</div>
)
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!name.trim()) {
alert("请输入收货人姓名")
return
}
if (!/^1[3-9]\d{9}$/.test(phone)) {
alert("请输入正确的手机号")
return
}
// 省/市/区为选填
if (!detail.trim()) {
alert("请输入详细地址")
return
}
setLoading(true)
try {
const res = await fetch("/api/user/addresses", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
userId: user.id,
name: name.trim(),
phone: phone.trim(),
province: (province ?? "").trim(),
city: (city ?? "").trim(),
district: (district ?? "").trim(),
detail: detail.trim(),
isDefault,
}),
})
const data = await res.json()
if (data.success) {
router.push("/view/my/addresses")
} else {
alert(data.message || "添加失败")
}
} catch {
alert("添加失败")
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen bg-black text-white pb-24">
<header className="sticky top-0 z-40 bg-black/90 backdrop-blur-xl border-b border-white/5">
<div className="px-4 py-3 flex items-center">
<button onClick={() => router.back()} className="w-8 h-8 rounded-full bg-white/10 flex items-center justify-center">
<ChevronLeft className="w-5 h-5 text-white" />
</button>
<h1 className="flex-1 text-center text-lg font-semibold text-white"></h1>
<div className="w-8" />
</div>
</header>
<form onSubmit={handleSubmit} className="px-4 py-4 space-y-4">
<div className="rounded-2xl bg-[#1c1c1e] border border-white/5 overflow-hidden">
<div className="flex items-center justify-between p-4 border-b border-white/5">
<label className="text-white text-sm w-24"></label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="请输入收货人姓名"
className="flex-1 bg-transparent text-white text-sm text-right placeholder-white/30 outline-none"
/>
</div>
<div className="flex items-center justify-between p-4 border-b border-white/5">
<label className="text-white text-sm w-24"></label>
<input
type="tel"
maxLength={11}
value={phone}
onChange={(e) => setPhone(e.target.value.replace(/\D/g, ""))}
placeholder="请输入手机号"
className="flex-1 bg-transparent text-white text-sm text-right placeholder-white/30 outline-none"
/>
</div>
<div className="flex items-center justify-between p-4 border-b border-white/5">
<label className="text-white text-sm w-24"></label>
<div className="flex-1 flex gap-2 justify-end">
<input
type="text"
value={province}
onChange={(e) => setProvince(e.target.value)}
placeholder="省"
className="flex-1 max-w-24 bg-transparent text-white text-sm text-right placeholder-white/30 outline-none"
/>
<input
type="text"
value={city}
onChange={(e) => setCity(e.target.value)}
placeholder="市"
className="flex-1 max-w-24 bg-transparent text-white text-sm text-right placeholder-white/30 outline-none"
/>
<input
type="text"
value={district}
onChange={(e) => setDistrict(e.target.value)}
placeholder="区"
className="flex-1 max-w-24 bg-transparent text-white text-sm text-right placeholder-white/30 outline-none"
/>
</div>
</div>
<div className="flex items-start justify-between p-4">
<label className="text-white text-sm w-24 pt-2"></label>
<textarea
value={detail}
onChange={(e) => setDetail(e.target.value)}
placeholder="街道、楼栋、门牌号等"
rows={3}
className="flex-1 bg-transparent text-white text-sm text-right placeholder-white/30 outline-none resize-none"
/>
</div>
<div className="flex items-center justify-between p-4 border-t border-white/5">
<span className="text-white text-sm"></span>
<input
type="checkbox"
checked={isDefault}
onChange={(e) => setIsDefault(e.target.checked)}
className="w-5 h-5 rounded accent-[#00CED1]"
/>
</div>
</div>
<button
type="submit"
disabled={loading}
className="w-full py-3 rounded-xl bg-[#00CED1] text-black font-medium disabled:opacity-50"
>
{loading ? "保存中..." : "保存"}
</button>
</form>
</div>
)
}

View File

@@ -0,0 +1,141 @@
"use client"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { ChevronLeft, MapPin, Plus, Pencil, Trash2 } from "lucide-react"
import { useStore } from "@/lib/store"
type AddressItem = {
id: string
name: string
phone: string
province: string
city: string
district: string
detail: string
fullAddress: string
isDefault: boolean
}
export default function AddressesPage() {
const router = useRouter()
const { user } = useStore()
const [list, setList] = useState<AddressItem[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
if (!user?.id) {
setList([])
setLoading(false)
return
}
fetch(`/api/user/addresses?userId=${user.id}`)
.then((res) => res.json())
.then((data) => {
if (data.success && data.list) setList(data.list)
setLoading(false)
})
.catch(() => setLoading(false))
}, [user?.id])
const handleDelete = async (id: string) => {
if (!confirm("确定要删除该收货地址吗?")) return
try {
const res = await fetch(`/api/user/addresses/${id}`, { method: "DELETE" })
const data = await res.json()
if (data.success) {
setList((prev) => prev.filter((a) => a.id !== id))
} else {
alert(data.message || "删除失败")
}
} catch {
alert("删除失败")
}
}
if (!user) {
return (
<div className="min-h-screen bg-black text-white flex items-center justify-center">
<div className="text-center">
<p className="text-white/60 mb-4"></p>
<button
onClick={() => router.push("/view/my")}
className="px-4 py-2 rounded-xl bg-[#00CED1] text-black font-medium"
>
</button>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-black text-white pb-24">
<header className="sticky top-0 z-40 bg-black/90 backdrop-blur-xl border-b border-white/5">
<div className="px-4 py-3 flex items-center">
<button
onClick={() => router.back()}
className="w-8 h-8 rounded-full bg-white/10 flex items-center justify-center"
>
<ChevronLeft className="w-5 h-5 text-white" />
</button>
<h1 className="flex-1 text-center text-lg font-semibold text-white"></h1>
<div className="w-8" />
</div>
</header>
<main className="px-4 py-4">
{loading ? (
<div className="py-12 text-center text-white/40 text-sm">...</div>
) : list.length === 0 ? (
<div className="py-12 text-center">
<MapPin className="w-12 h-12 text-white/30 mx-auto mb-3" />
<p className="text-white/60 text-sm"></p>
<p className="text-white/40 text-xs mt-1"></p>
</div>
) : (
<div className="space-y-3">
{list.map((item) => (
<div
key={item.id}
className="rounded-xl bg-[#1c1c1e] border border-white/5 p-4"
>
<div className="flex items-center justify-between mb-2">
<span className="text-white font-medium">{item.name}</span>
<span className="text-white/50 text-sm">{item.phone}</span>
{item.isDefault && (
<span className="text-xs px-2 py-0.5 rounded bg-[#00CED1]/20 text-[#00CED1]">
</span>
)}
</div>
<p className="text-white/60 text-sm leading-relaxed">{item.fullAddress}</p>
<div className="flex justify-end gap-4 mt-3 pt-3 border-t border-white/5">
<button
onClick={() => router.push(`/view/my/addresses/${item.id}`)}
className="flex items-center gap-1 text-[#00CED1] text-sm"
>
<Pencil className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(item.id)}
className="flex items-center gap-1 text-red-400 text-sm"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
))}
</div>
)}
<button
onClick={() => router.push("/view/my/addresses/new")}
className="mt-6 w-full py-3 rounded-xl bg-[#00CED1] text-black font-medium flex items-center justify-center gap-2"
>
<Plus className="w-5 h-5" />
</button>
</main>
</div>
)
}

551
app/view/my/page.tsx Normal file
View File

@@ -0,0 +1,551 @@
"use client"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { User, Users, ChevronRight, Gift, Star, Info, Wallet, Footprints, Eye, BookOpen, Clock, ArrowUpRight, Phone, MessageCircle, CreditCard, X, Check, Loader2, Settings } from "lucide-react"
import { useStore } from "@/lib/store"
import { AuthModal } from "@/components/modules/auth/auth-modal"
import { getFullBookPrice, getTotalSectionCount } from "@/lib/book-data"
export default function MyPage() {
const router = useRouter()
const { user, isLoggedIn, logout, getAllPurchases, settings, updateUser, refreshUserProfile } = useStore()
const [showAuthModal, setShowAuthModal] = useState(false)
const [mounted, setMounted] = useState(false)
const [activeTab, setActiveTab] = useState<"overview" | "footprint">("overview")
const [matchEnabled, setMatchEnabled] = useState(false) // 匹配功能是否启用
// 绑定弹窗状态
const [showBindModal, setShowBindModal] = useState(false)
const [bindType, setBindType] = useState<"phone" | "wechat" | "alipay">("phone")
const [bindValue, setBindValue] = useState("")
const [isBinding, setIsBinding] = useState(false)
const [bindError, setBindError] = useState("")
// 计算数据(必须在所有 hooks 之后)
const totalSections = getTotalSectionCount()
const purchasedCount = user?.hasFullBook ? totalSections : user?.purchasedSections?.length || 0
useEffect(() => {
setMounted(true)
// 加载功能配置
const loadConfig = async () => {
try {
const res = await fetch('/api/db/config')
const data = await res.json()
if (data.features) {
setMatchEnabled(data.features.matchEnabled === true)
}
} catch (e) {
console.log('Load feature config error:', e)
setMatchEnabled(false)
}
}
loadConfig()
}, [])
// 每次进入「我的」页都刷新可提现金额等用户资料
useEffect(() => {
if (mounted && isLoggedIn && user?.id && refreshUserProfile) {
refreshUserProfile()
}
}, [mounted, isLoggedIn, user?.id])
// 绑定账号
const handleBind = async () => {
if (!bindValue.trim()) {
setBindError("请输入内容")
return
}
if (bindType === "phone" && !/^1[3-9]\d{9}$/.test(bindValue)) {
setBindError("请输入正确的手机号")
return
}
if (bindType === "wechat" && bindValue.length < 6) {
setBindError("微信号至少6位")
return
}
if (bindType === "alipay" && !bindValue.includes("@") && !/^1[3-9]\d{9}$/.test(bindValue)) {
setBindError("请输入正确的支付宝账号")
return
}
setIsBinding(true)
setBindError("")
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000))
// 更新用户信息
if (updateUser && user) {
const updates: any = {}
if (bindType === "phone") updates.phone = bindValue
if (bindType === "wechat") updates.wechat = bindValue
if (bindType === "alipay") updates.alipay = bindValue
updateUser(user.id, updates)
}
setShowBindModal(false)
setBindValue("")
alert("绑定成功!")
} catch (error) {
setBindError("绑定失败,请重试")
} finally {
setIsBinding(false)
}
}
// 打开绑定弹窗
const openBindModal = (type: "phone" | "wechat" | "alipay") => {
setBindType(type)
setBindValue("")
setBindError("")
setShowBindModal(true)
}
// 等待挂载完成
if (!mounted) {
return (
<div className="min-h-screen bg-black flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-[#00CED1]" />
</div>
)
}
// 未登录状态
if (!isLoggedIn) {
return (
<main className="min-h-screen bg-black text-white pb-24">
<div className="text-center py-4 border-b border-white/10">
<h1 className="text-lg font-medium text-[#00CED1]"></h1>
</div>
{/* 用户卡片 */}
<div className="mx-4 mt-4 p-4 rounded-2xl bg-gradient-to-br from-[#1c1c1e] to-[#2c2c2e] border border-[#00CED1]/20">
<div className="flex items-center gap-3 mb-4">
<div className="w-16 h-16 rounded-full border-2 border-dashed border-[#00CED1]/50 flex items-center justify-center bg-gradient-to-br from-[#00CED1]/10 to-transparent">
<User className="w-8 h-8 text-white/30" />
</div>
<div className="flex-1">
<button onClick={() => setShowAuthModal(true)} className="text-[#00CED1] font-semibold text-lg">
</button>
<p className="text-white/30 text-sm"></p>
</div>
</div>
<div className="grid grid-cols-3 gap-2 pt-4 border-t border-white/10">
<div className="text-center p-2 rounded-lg bg-white/5">
<p className="text-[#00CED1] text-xl font-bold">0</p>
<p className="text-white/40 text-xs"></p>
</div>
<div className="text-center p-2 rounded-lg bg-white/5">
<p className="text-[#00CED1] text-xl font-bold">0</p>
<p className="text-white/40 text-xs"></p>
</div>
<div className="text-center p-2 rounded-lg bg-white/5">
<p className="text-[#FFD700] text-xl font-bold">--</p>
<p className="text-white/40 text-xs"></p>
</div>
</div>
</div>
{/* 分销入口 */}
<div className="mx-4 mt-4 p-4 rounded-2xl bg-gradient-to-r from-[#FFD700]/10 via-[#1c1c1e] to-[#1c1c1e] border border-[#FFD700]/20">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-[#FFD700] to-[#FFA500] flex items-center justify-center">
<Gift className="w-5 h-5 text-black" />
</div>
<div>
<p className="text-white font-medium">广</p>
<p className="text-white/40 text-xs"></p>
</div>
</div>
<button
onClick={() => setShowAuthModal(true)}
className="px-4 py-2 rounded-lg bg-[#FFD700]/20 text-[#FFD700] text-sm font-medium"
>
</button>
</div>
</div>
{/* 菜单列表 */}
<div className="mx-4 mt-4 rounded-2xl bg-[#1c1c1e] border border-white/5 overflow-hidden">
<button
onClick={() => setShowAuthModal(true)}
className="w-full flex items-center justify-between p-4 border-b border-white/5 active:bg-white/5"
>
<div className="flex items-center gap-3">
<span className="text-xl">📦</span>
<span className="text-white"></span>
</div>
<ChevronRight className="w-5 h-5 text-white/30" />
</button>
<button
onClick={() => router.push("/view/about")}
className="w-full flex items-center justify-between p-4 active:bg-white/5"
>
<div className="flex items-center gap-3">
<div className="w-6 h-6 rounded-full bg-gradient-to-br from-[#00CED1] to-[#20B2AA] flex items-center justify-center">
<Info className="w-3 h-3 text-white" />
</div>
<span className="text-white"></span>
</div>
<ChevronRight className="w-5 h-5 text-white/30" />
</button>
</div>
<AuthModal isOpen={showAuthModal} onClose={() => setShowAuthModal(false)} />
</main>
)
}
// 已登录状态
const userPurchases = getAllPurchases().filter((p) => p.userId === user?.id)
const completedOrders = userPurchases.filter((p) => p.status === "completed").length
// 模拟足迹数据(实际应从数据库获取)
const footprintData = {
recentChapters: user?.purchasedSections?.slice(-5) || [],
matchHistory: [], // 匹配历史
totalReadTime: Math.floor(Math.random() * 200) + 50, // 阅读时长(分钟)
}
return (
<main className="min-h-screen bg-black text-white pb-24">
<div className="text-center py-4 border-b border-white/10">
<h1 className="text-lg font-medium text-[#00CED1]"></h1>
</div>
{/* 用户卡片 */}
<div className="mx-4 mt-4 p-4 rounded-2xl bg-gradient-to-br from-[#1c1c1e] to-[#2c2c2e] border border-[#00CED1]/20">
<div className="flex items-center gap-3 mb-4">
<div className="w-16 h-16 rounded-full border-2 border-[#00CED1] flex items-center justify-center bg-gradient-to-br from-[#00CED1]/20 to-transparent">
<span className="text-2xl font-bold text-[#00CED1]">{user?.nickname?.charAt(0) || "U"}</span>
</div>
<div className="flex-1">
<p className="text-white font-semibold text-lg">{user?.nickname || "用户"}</p>
<p className="text-white/30 text-sm">ID: {user?.id?.slice(-8) || "---"}</p>
</div>
<div className="px-3 py-1 rounded-full bg-[#00CED1]/20 border border-[#00CED1]/30">
<span className="text-[#00CED1] text-xs flex items-center gap-1">
<Star className="w-3 h-3" />
</span>
</div>
</div>
<div className="grid grid-cols-3 gap-2 pt-4 border-t border-white/10">
<div className="text-center p-2 rounded-lg bg-white/5">
<p className="text-[#00CED1] text-xl font-bold">{purchasedCount}</p>
<p className="text-white/40 text-xs"></p>
</div>
<div className="text-center p-2 rounded-lg bg-white/5">
<p className="text-[#00CED1] text-xl font-bold">{user?.referralCount || 0}</p>
<p className="text-white/40 text-xs"></p>
</div>
<div className="text-center p-2 rounded-lg bg-white/5">
<p className="text-[#FFD700] text-xl font-bold">
{(user?.earnings || 0) > 0 ? `¥${(user?.earnings || 0).toFixed(0)}` : '--'}
</p>
<p className="text-white/40 text-xs"></p>
</div>
</div>
</div>
{/* 收益卡片 - 艺术化设计 */}
<div className="mx-4 mt-4 p-4 rounded-2xl bg-gradient-to-br from-[#1a1a2e] via-[#16213e] to-[#0f3460] border border-[#00CED1]/20 relative overflow-hidden">
{/* 背景装饰 */}
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-[#FFD700]/10 to-transparent rounded-full -translate-y-1/2 translate-x-1/2" />
<div className="absolute bottom-0 left-0 w-24 h-24 bg-gradient-to-tr from-[#00CED1]/10 to-transparent rounded-full translate-y-1/2 -translate-x-1/2" />
<div className="relative">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Wallet className="w-5 h-5 text-[#FFD700]" />
<span className="text-white font-medium"></span>
</div>
<button
onClick={() => router.push("/view/my/referral")}
className="text-[#00CED1] text-xs flex items-center gap-1"
>
广
<ArrowUpRight className="w-3 h-3" />
</button>
</div>
<div className="flex items-end gap-6 mb-4">
<div>
<p className="text-white/50 text-xs mb-1"></p>
<p className="text-3xl font-bold bg-gradient-to-r from-[#FFD700] to-[#FFA500] bg-clip-text text-transparent">
¥{(user?.earnings || 0).toFixed(2)}
</p>
</div>
<div className="flex-1">
<p className="text-white/50 text-xs mb-1"></p>
<p className="text-xl font-semibold text-white">
¥{(user?.pendingEarnings || 0).toFixed(2)}
</p>
</div>
</div>
<button
onClick={() => router.push("/view/my/referral")}
className="w-full py-2.5 rounded-xl bg-gradient-to-r from-[#FFD700]/80 to-[#FFA500]/80 text-black text-sm font-bold flex items-center justify-center gap-2"
>
<Gift className="w-4 h-4" />
广 /
</button>
</div>
</div>
{/* Tab切换 */}
<div className="mx-4 mt-4 flex gap-2">
<button
onClick={() => setActiveTab("overview")}
className={`flex-1 py-2.5 rounded-xl text-sm font-medium transition-colors ${
activeTab === "overview"
? "bg-[#00CED1]/20 text-[#00CED1] border border-[#00CED1]/30"
: "bg-[#1c1c1e] text-white/60"
}`}
>
</button>
<button
onClick={() => setActiveTab("footprint")}
className={`flex-1 py-2.5 rounded-xl text-sm font-medium transition-colors flex items-center justify-center gap-1 ${
activeTab === "footprint"
? "bg-[#00CED1]/20 text-[#00CED1] border border-[#00CED1]/30"
: "bg-[#1c1c1e] text-white/60"
}`}
>
<Footprints className="w-4 h-4" />
</button>
</div>
{activeTab === "overview" ? (
<>
{/* 菜单列表 */}
<div className="mx-4 mt-4 rounded-2xl bg-[#1c1c1e] border border-white/5 overflow-hidden">
<button
onClick={() => router.push("/view/my/purchases")}
className="w-full flex items-center justify-between p-4 border-b border-white/5 active:bg-white/5"
>
<div className="flex items-center gap-3">
<span className="text-xl">📦</span>
<span className="text-white"></span>
</div>
<div className="flex items-center gap-2">
<span className="text-white/40 text-sm">{completedOrders}</span>
<ChevronRight className="w-5 h-5 text-white/30" />
</div>
</button>
<button
onClick={() => router.push("/view/my/referral")}
className="w-full flex items-center justify-between p-4 border-b border-white/5 active:bg-white/5"
>
<div className="flex items-center gap-3">
<div className="w-6 h-6 rounded-full bg-gradient-to-br from-[#FFD700] to-[#FFA500] flex items-center justify-center">
<Gift className="w-3 h-3 text-black" />
</div>
<span className="text-white">广</span>
</div>
<div className="flex items-center gap-2">
<span className="text-[#FFD700] text-sm font-medium">90%</span>
<ChevronRight className="w-5 h-5 text-white/30" />
</div>
</button>
<button
onClick={() => router.push("/view/about")}
className="w-full flex items-center justify-between p-4 active:bg-white/5"
>
<div className="flex items-center gap-3">
<div className="w-6 h-6 rounded-full bg-gradient-to-br from-[#00CED1] to-[#20B2AA] flex items-center justify-center">
<Info className="w-3 h-3 text-white" />
</div>
<span className="text-white"></span>
</div>
<ChevronRight className="w-5 h-5 text-white/30" />
</button>
<button
onClick={() => router.push("/view/my/settings")}
className="w-full flex items-center justify-between p-4 active:bg-white/5"
>
<div className="flex items-center gap-3">
<div className="w-6 h-6 rounded-full bg-gray-500/20 flex items-center justify-center">
<Settings className="w-3 h-3 text-gray-400" />
</div>
<span className="text-white"></span>
</div>
<ChevronRight className="w-5 h-5 text-white/30" />
</button>
</div>
</>
) : (
<>
{/* 足迹内容 */}
<div className="mx-4 mt-4 space-y-4">
{/* 阅读统计 */}
<div className="p-4 rounded-2xl bg-[#1c1c1e] border border-white/5">
<h3 className="text-white font-medium mb-3 flex items-center gap-2">
<Eye className="w-4 h-4 text-[#00CED1]" />
</h3>
<div className="grid grid-cols-3 gap-3">
<div className="text-center p-3 rounded-xl bg-white/5">
<BookOpen className="w-5 h-5 text-[#00CED1] mx-auto mb-1" />
<p className="text-white font-bold">{purchasedCount}</p>
<p className="text-white/40 text-xs"></p>
</div>
<div className="text-center p-3 rounded-xl bg-white/5">
<Clock className="w-5 h-5 text-[#FFD700] mx-auto mb-1" />
<p className="text-white font-bold">{footprintData.totalReadTime}</p>
<p className="text-white/40 text-xs"></p>
</div>
<div className="text-center p-3 rounded-xl bg-white/5">
<Users className="w-5 h-5 text-[#E91E63] mx-auto mb-1" />
<p className="text-white font-bold">{footprintData.matchHistory.length || 0}</p>
<p className="text-white/40 text-xs"></p>
</div>
</div>
</div>
{/* 最近阅读 */}
<div className="p-4 rounded-2xl bg-[#1c1c1e] border border-white/5">
<h3 className="text-white font-medium mb-3 flex items-center gap-2">
<BookOpen className="w-4 h-4 text-[#00CED1]" />
</h3>
{footprintData.recentChapters.length > 0 ? (
<div className="space-y-2">
{footprintData.recentChapters.map((sectionId, index) => (
<div
key={sectionId}
className="flex items-center justify-between p-3 rounded-xl bg-white/5"
>
<div className="flex items-center gap-3">
<span className="text-white/30 text-sm">{index + 1}</span>
<span className="text-white text-sm"> {sectionId}</span>
</div>
<button
onClick={() => router.push(`/view/read/${sectionId}`)}
className="text-[#00CED1] text-xs"
>
</button>
</div>
))}
</div>
) : (
<div className="text-center py-6 text-white/40">
<BookOpen className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p className="text-sm"></p>
<button
onClick={() => router.push("/view/chapters")}
className="mt-2 text-[#00CED1] text-sm"
>
</button>
</div>
)}
</div>
{/* 匹配记录 - 根据配置显示 */}
{matchEnabled && (
<div className="p-4 rounded-2xl bg-[#1c1c1e] border border-white/5">
<h3 className="text-white font-medium mb-3 flex items-center gap-2">
<Users className="w-4 h-4 text-[#00CED1]" />
</h3>
<div className="text-center py-6 text-white/40">
<Users className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p className="text-sm"></p>
<button
onClick={() => router.push("/view/match")}
className="mt-2 text-[#00CED1] text-sm"
>
</button>
</div>
</div>
)}
</div>
</>
)}
{/* 绑定弹窗 */}
{showBindModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={() => !isBinding && setShowBindModal(false)} />
<div className="relative w-full max-w-sm bg-[#1c1c1e] rounded-2xl overflow-hidden">
<div className="flex items-center justify-between p-4 border-b border-white/10">
<h3 className="text-lg font-semibold text-white">
{bindType === "phone" ? "手机号" : bindType === "wechat" ? "微信号" : "支付宝"}
</h3>
<button
onClick={() => !isBinding && setShowBindModal(false)}
className="w-8 h-8 rounded-full bg-white/10 flex items-center justify-center"
>
<X className="w-4 h-4 text-white/60" />
</button>
</div>
<div className="p-5">
<div className="mb-4">
<label className="block text-white/40 text-xs mb-2">
{bindType === "phone" ? "手机号" : bindType === "wechat" ? "微信号" : "支付宝账号"}
</label>
<input
type={bindType === "phone" ? "tel" : "text"}
value={bindValue}
onChange={(e) => setBindValue(e.target.value)}
placeholder={
bindType === "phone" ? "请输入11位手机号" :
bindType === "wechat" ? "请输入微信号" :
"请输入支付宝账号"
}
className="w-full px-4 py-3 rounded-xl bg-black/30 border border-white/10 text-white placeholder-white/30 focus:outline-none focus:border-[#00CED1]/50"
disabled={isBinding}
/>
</div>
{bindError && (
<p className="text-red-400 text-sm mb-4">{bindError}</p>
)}
<p className="text-white/40 text-xs mb-4">
{bindType === "phone" && "绑定手机号后可用于找伙伴匹配"}
{bindType === "wechat" && "绑定微信号后可用于找伙伴匹配和好友添加"}
{bindType === "alipay" && "绑定支付宝后可用于提现收益"}
</p>
<button
onClick={handleBind}
disabled={isBinding || !bindValue}
className="w-full py-3 rounded-xl bg-[#00CED1] text-black font-medium flex items-center justify-center gap-2 disabled:opacity-50"
>
{isBinding ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
...
</>
) : (
"确认绑定"
)}
</button>
</div>
</div>
</div>
)}
</main>
)
}

View File

@@ -0,0 +1,109 @@
"use client"
import Link from "next/link"
import { ChevronLeft, BookOpen, CheckCircle } from "lucide-react"
import { useStore } from "@/lib/store"
import { bookData, getAllSections } from "@/lib/book-data"
export default function MyPurchasesPage() {
const { user, isLoggedIn } = useStore()
if (!isLoggedIn || !user) {
return (
<div className="min-h-screen bg-[#0a1628] text-white flex items-center justify-center">
<div className="text-center">
<p className="text-gray-400 mb-4"></p>
<Link href="/view" className="text-[#38bdac] hover:underline">
</Link>
</div>
</div>
)
}
const allSections = getAllSections()
const purchasedCount = user.hasFullBook ? allSections.length : user.purchasedSections.length
return (
<div className="min-h-screen bg-[#0a1628] text-white">
{/* Header */}
<header className="sticky top-0 z-50 bg-[#0a1628]/90 backdrop-blur-md border-b border-gray-800">
<div className="max-w-4xl mx-auto px-4 py-4 flex items-center">
<Link href="/view" className="flex items-center gap-2 text-gray-400 hover:text-white transition-colors">
<ChevronLeft className="w-5 h-5" />
<span></span>
</Link>
<h1 className="flex-1 text-center text-lg font-semibold"></h1>
<div className="w-16" />
</div>
</header>
<main className="max-w-4xl mx-auto px-4 py-8">
{/* Stats */}
<div className="bg-[#0f2137]/60 backdrop-blur-md rounded-xl p-6 border border-gray-700/50 mb-8">
<div className="grid grid-cols-2 gap-6">
<div className="text-center">
<p className="text-3xl font-bold text-white">{purchasedCount}</p>
<p className="text-gray-400 text-sm"></p>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-[#38bdac]">
{user.hasFullBook ? "全书" : `${purchasedCount}/${allSections.length}`}
</p>
<p className="text-gray-400 text-sm">{user.hasFullBook ? "已解锁" : "进度"}</p>
</div>
</div>
</div>
{/* Purchased sections */}
{user.hasFullBook ? (
<div className="bg-gradient-to-r from-[#38bdac]/20 to-[#0f2137] rounded-xl p-6 border border-[#38bdac]/30 text-center mb-8">
<CheckCircle className="w-12 h-12 text-[#38bdac] mx-auto mb-3" />
<h3 className="text-xl font-semibold text-white mb-2"></h3>
<p className="text-gray-400">55,</p>
</div>
) : user.purchasedSections.length === 0 ? (
<div className="text-center py-12">
<BookOpen className="w-16 h-16 text-gray-600 mx-auto mb-4" />
<p className="text-gray-400 mb-4"></p>
<Link href="/view/chapters" className="text-[#38bdac] hover:underline">
</Link>
</div>
) : (
<div className="space-y-4">
<h2 className="text-gray-400 text-sm mb-4"></h2>
{bookData.map((part) => {
const purchasedInPart = part.chapters.flatMap((c) =>
c.sections.filter((s) => user.purchasedSections.includes(s.id)),
)
if (purchasedInPart.length === 0) return null
return (
<div key={part.id} className="bg-[#0f2137]/40 rounded-xl border border-gray-800/50 overflow-hidden">
<div className="px-4 py-3 bg-[#0a1628]/50">
<p className="text-gray-400 text-sm">{part.title}</p>
</div>
<div className="divide-y divide-gray-800/30">
{purchasedInPart.map((section) => (
<Link
key={section.id}
href={`/view/read/${section.id}`}
className="flex items-center gap-3 px-4 py-3 hover:bg-[#0f2137]/40 transition-colors"
>
<CheckCircle className="w-4 h-4 text-[#38bdac]" />
<span className="text-gray-400 font-mono text-sm">{section.id}</span>
<span className="text-gray-300">{section.title}</span>
</Link>
))}
</div>
</div>
)
})}
</div>
)}
</main>
</div>
)
}

View File

@@ -0,0 +1,569 @@
"use client"
import { useState, useEffect } from "react"
import Link from "next/link"
import {
ChevronLeft, Copy, Share2, Users, Wallet, MessageCircle, ImageIcon,
TrendingUp, Gift, Check, ArrowRight, Bell, Clock, AlertCircle,
CheckCircle, UserPlus, Settings, Zap, ChevronDown, ChevronUp
} from "lucide-react"
import { useStore, type Purchase } from "@/lib/store"
import { PosterModal } from "@/components/modules/referral/poster-modal"
import { WithdrawalModal } from "@/components/modules/referral/withdrawal-modal"
import { AutoWithdrawModal } from "@/components/modules/distribution/auto-withdraw-modal"
import { RealtimeNotification } from "@/components/modules/distribution/realtime-notification"
// 绑定用户类型
interface BindingUser {
id: string
visitorNickname?: string
visitorPhone?: string
bindingTime: string
expireTime: string
status: 'active' | 'converted' | 'expired'
daysRemaining?: number
commission?: number
orderAmount?: number
}
export default function ReferralPage() {
const { user, isLoggedIn, settings, getAllPurchases, getAllUsers } = useStore()
const [copied, setCopied] = useState(false)
const [showPoster, setShowPoster] = useState(false)
const [showWithdrawal, setShowWithdrawal] = useState(false)
const [showAutoWithdraw, setShowAutoWithdraw] = useState(false)
const [referralPurchases, setReferralPurchases] = useState<Purchase[]>([])
const [referralUsers, setReferralUsers] = useState<number>(0)
// 绑定用户相关状态
const [activeBindings, setActiveBindings] = useState<BindingUser[]>([])
const [convertedBindings, setConvertedBindings] = useState<BindingUser[]>([])
const [expiredBindings, setExpiredBindings] = useState<BindingUser[]>([])
const [expiringCount, setExpiringCount] = useState(0)
const [showBindingList, setShowBindingList] = useState(true)
const [activeTab, setActiveTab] = useState<'active' | 'converted' | 'expired'>('active')
// 自动提现状态
const [autoWithdrawEnabled, setAutoWithdrawEnabled] = useState(false)
const [autoWithdrawThreshold, setAutoWithdrawThreshold] = useState(100)
useEffect(() => {
if (user?.referralCode) {
const allPurchases = getAllPurchases()
const allUsers = getAllUsers()
const usersWithMyCode = allUsers.filter((u) => u.referredBy === user.referralCode)
const userIds = usersWithMyCode.map((u) => u.id)
const myReferralPurchases = allPurchases.filter((p) => userIds.includes(p.userId))
setReferralPurchases(myReferralPurchases)
setReferralUsers(usersWithMyCode.length)
// 模拟绑定数据实际从API获取
loadBindingData()
}
}, [user, getAllPurchases, getAllUsers])
// 加载绑定数据
const loadBindingData = async () => {
// 模拟数据 - 实际项目中从 /api/distribution?type=my-bindings&userId=xxx 获取
const now = new Date()
const mockActiveBindings: BindingUser[] = [
{
id: '1',
visitorNickname: '小明',
visitorPhone: '138****1234',
bindingTime: new Date(Date.now() - 25 * 24 * 60 * 60 * 1000).toISOString(),
expireTime: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000).toISOString(),
status: 'active',
daysRemaining: 5,
},
{
id: '2',
visitorNickname: '小红',
visitorPhone: '139****5678',
bindingTime: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString(),
expireTime: new Date(Date.now() + 20 * 24 * 60 * 60 * 1000).toISOString(),
status: 'active',
daysRemaining: 20,
},
{
id: '3',
visitorNickname: '阿强',
visitorPhone: '137****9012',
bindingTime: new Date(Date.now() - 28 * 24 * 60 * 60 * 1000).toISOString(),
expireTime: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString(),
status: 'active',
daysRemaining: 2,
},
]
const mockConvertedBindings: BindingUser[] = [
{
id: '4',
visitorNickname: '小李',
visitorPhone: '136****3456',
bindingTime: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000).toISOString(),
expireTime: new Date(Date.now() + 15 * 24 * 60 * 60 * 1000).toISOString(),
status: 'converted',
commission: 8.91,
orderAmount: 9.9,
},
]
const mockExpiredBindings: BindingUser[] = [
{
id: '5',
visitorNickname: '小王',
visitorPhone: '135****7890',
bindingTime: new Date(Date.now() - 35 * 24 * 60 * 60 * 1000).toISOString(),
expireTime: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(),
status: 'expired',
},
]
setActiveBindings(mockActiveBindings)
setConvertedBindings(mockConvertedBindings)
setExpiredBindings(mockExpiredBindings)
setExpiringCount(mockActiveBindings.filter(b => (b.daysRemaining || 0) <= 7).length)
}
if (!isLoggedIn || !user) {
return (
<div className="min-h-screen bg-black text-white flex items-center justify-center pb-20">
<div className="text-center glass-card p-8">
<p className="text-[var(--app-text-secondary)] mb-4"></p>
<Link href="/view" className="btn-ios inline-block">
</Link>
</div>
</div>
)
}
const referralLink = `${typeof window !== "undefined" ? window.location.origin : ""}?ref=${user.referralCode}`
const distributorShare = settings?.distributorShare || 90
const totalEarnings = user.earnings || 0
const pendingEarnings = user.pendingEarnings || 0
const handleCopy = () => {
navigator.clipboard.writeText(referralLink)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
const handleShare = async () => {
const shareText = `我正在读《一场SOUL的创业实验场》每天6-9点的真实商业故事推荐给你${referralLink}`
try {
if (typeof navigator.share === 'function' && typeof navigator.canShare === 'function') {
await navigator.share({
title: "一场SOUL的创业实验场",
text: "来自Soul派对房的真实商业故事",
url: referralLink,
})
} else {
await navigator.clipboard.writeText(shareText)
alert("分享文案已复制快去朋友圈或Soul派对分享吧")
}
} catch {
await navigator.clipboard.writeText(shareText)
alert("分享文案已复制!")
}
}
const handleShareToWechat = async () => {
const shareText = `📖 推荐一本好书《一场SOUL的创业实验场》
这是卡若每天早上6-9点在Soul派对房分享的真实商业故事55个真实案例讲透创业的底层逻辑。
👉 点击阅读: ${referralLink}
#创业 #商业思维 #Soul派对`
await navigator.clipboard.writeText(shareText)
alert("朋友圈文案已复制!\n\n打开微信 → 发朋友圈 → 粘贴即可")
}
// 获取绑定状态样式
const getBindingStatusStyle = (daysRemaining?: number) => {
if (!daysRemaining) return 'bg-gray-500/20 text-gray-400'
if (daysRemaining <= 3) return 'bg-red-500/20 text-red-400'
if (daysRemaining <= 7) return 'bg-orange-500/20 text-orange-400'
return 'bg-green-500/20 text-green-400'
}
// 获取绑定状态文本
const getBindingStatusText = (binding: BindingUser) => {
if (binding.status === 'converted') return '已付款'
if (binding.status === 'expired') return '已过期'
if (binding.daysRemaining && binding.daysRemaining <= 3) return `${binding.daysRemaining}天后过期`
if (binding.daysRemaining && binding.daysRemaining <= 7) return `${binding.daysRemaining}`
return `${binding.daysRemaining || 0}`
}
const currentBindings = activeTab === 'active' ? activeBindings : activeTab === 'converted' ? convertedBindings : expiredBindings
return (
<div className="min-h-screen bg-black text-white pb-24 page-transition">
{/* 背景光效 */}
<div className="fixed inset-0 -z-10">
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[500px] h-[500px] bg-[var(--app-brand)] opacity-[0.05] blur-[150px] rounded-full" />
</div>
{/* Header - iOS风格 */}
<header className="sticky top-0 z-50 glass-nav safe-top">
<div className="max-w-md mx-auto px-4 py-3 flex items-center">
<Link href="/view/my" className="w-8 h-8 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>
<h1 className="flex-1 text-center font-semibold"></h1>
<div className="flex items-center gap-2">
<RealtimeNotification />
<button
onClick={() => setShowAutoWithdraw(true)}
className="w-8 h-8 rounded-full bg-[var(--app-bg-secondary)] flex items-center justify-center touch-feedback"
>
<Settings className="w-4 h-4 text-[var(--app-text-secondary)]" />
</button>
</div>
</div>
</header>
<main className="max-w-md mx-auto px-4 py-6">
{/* 过期提醒横幅 */}
{expiringCount > 0 && (
<div className="mb-4 glass-card p-4 border border-orange-500/30 bg-orange-500/10">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-orange-500/20 flex items-center justify-center flex-shrink-0">
<Bell className="w-5 h-5 text-orange-400" />
</div>
<div className="flex-1">
<p className="text-white font-medium text-sm">
{expiringCount}
</p>
<p className="text-orange-300/80 text-xs mt-0.5">
30
</p>
</div>
</div>
</div>
)}
{/* 收益卡片 - 毛玻璃渐变 */}
<div className="relative glass-card-heavy p-6 mb-6 overflow-hidden">
{/* 背景装饰 */}
<div className="absolute top-0 right-0 w-32 h-32 bg-[var(--app-brand)] opacity-[0.15] blur-[50px] rounded-full" />
<div className="relative">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<div className="w-10 h-10 rounded-xl bg-[var(--app-brand-light)] flex items-center justify-center">
<Wallet className="w-5 h-5 text-[var(--app-brand)]" />
</div>
<div>
<p className="text-[var(--app-text-tertiary)] text-xs"></p>
<p className="text-[var(--app-brand)] text-xs font-medium">{distributorShare}% </p>
</div>
</div>
<div className="text-right">
<p className="text-3xl font-bold text-white glow-text">¥{totalEarnings.toFixed(2)}</p>
<p className="text-[var(--app-text-tertiary)] text-xs">: ¥{pendingEarnings.toFixed(2)}</p>
</div>
</div>
<div className="flex gap-2">
<button
disabled={totalEarnings < 10}
onClick={() => setShowWithdrawal(true)}
className="flex-1 btn-ios glow disabled:opacity-50 disabled:cursor-not-allowed"
>
{totalEarnings < 10 ? `满10元可提现` : "申请提现"}
</button>
{autoWithdrawEnabled && (
<div className="flex items-center gap-1 px-3 py-2 bg-[var(--app-brand-light)] rounded-xl">
<Zap className="w-4 h-4 text-[var(--app-brand)]" />
<span className="text-[var(--app-brand)] text-xs font-medium"></span>
</div>
)}
</div>
</div>
</div>
{/* 数据统计 */}
<div className="grid grid-cols-4 gap-2 mb-6">
<div className="glass-card p-3 text-center">
<p className="text-xl font-bold text-white">{activeBindings.length}</p>
<p className="text-[var(--app-text-tertiary)] text-[10px]"></p>
</div>
<div className="glass-card p-3 text-center">
<p className="text-xl font-bold text-white">{convertedBindings.length}</p>
<p className="text-[var(--app-text-tertiary)] text-[10px]"></p>
</div>
<div className="glass-card p-3 text-center">
<p className="text-xl font-bold text-orange-400">{expiringCount}</p>
<p className="text-[var(--app-text-tertiary)] text-[10px]"></p>
</div>
<div className="glass-card p-3 text-center">
<p className="text-xl font-bold text-white">{referralUsers}</p>
<p className="text-[var(--app-text-tertiary)] text-[10px]"></p>
</div>
</div>
{/* 分销规则说明 */}
<div className="glass-card p-4 mb-6 border border-[var(--app-brand)]/20 bg-[var(--app-brand)]/5">
<div className="flex items-start gap-3">
<div className="w-8 h-8 rounded-lg bg-[var(--app-brand-light)] flex items-center justify-center flex-shrink-0">
<AlertCircle className="w-4 h-4 text-[var(--app-brand)]" />
</div>
<div>
<p className="text-white font-medium text-sm mb-1">广</p>
<ul className="text-[var(--app-text-tertiary)] text-xs space-y-1">
<li> <span className="text-[#FFD700]">5%</span></li>
<li> <span className="text-[var(--app-brand)]">{distributorShare}%</span> </li>
<li> <span className="text-[var(--app-brand)]">30</span></li>
</ul>
</div>
</div>
</div>
{/* 绑定用户列表 */}
<div className="glass-card overflow-hidden mb-6">
<button
onClick={() => setShowBindingList(!showBindingList)}
className="w-full px-5 py-4 flex items-center justify-between border-b border-[var(--app-separator)]"
>
<div className="flex items-center gap-2">
<Users className="w-5 h-5 text-[var(--app-brand)]" />
<h3 className="text-white font-semibold"></h3>
<span className="text-[var(--app-text-tertiary)] text-sm">
({activeBindings.length + convertedBindings.length + expiredBindings.length})
</span>
</div>
{showBindingList ? (
<ChevronUp className="w-5 h-5 text-[var(--app-text-tertiary)]" />
) : (
<ChevronDown className="w-5 h-5 text-[var(--app-text-tertiary)]" />
)}
</button>
{showBindingList && (
<>
{/* Tab切换 */}
<div className="flex border-b border-[var(--app-separator)]">
{[
{ key: 'active', label: '绑定中', count: activeBindings.length },
{ key: 'converted', label: '已付款', count: convertedBindings.length },
{ key: 'expired', label: '已过期', count: expiredBindings.length },
].map((tab) => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key as typeof activeTab)}
className={`flex-1 py-3 text-sm font-medium transition-colors relative ${
activeTab === tab.key
? 'text-[var(--app-brand)]'
: 'text-[var(--app-text-tertiary)]'
}`}
>
{tab.label} ({tab.count})
{activeTab === tab.key && (
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-12 h-0.5 bg-[var(--app-brand)] rounded-full" />
)}
</button>
))}
</div>
{/* 用户列表 */}
<div className="max-h-80 overflow-auto scrollbar-hide">
{currentBindings.length === 0 ? (
<div className="py-12 text-center">
<UserPlus className="w-10 h-10 text-[var(--app-text-tertiary)] mx-auto mb-2" />
<p className="text-[var(--app-text-tertiary)] text-sm"></p>
</div>
) : (
currentBindings.map((binding, idx) => (
<div
key={binding.id}
className={`px-5 py-4 flex items-center justify-between ${
idx !== currentBindings.length - 1 ? 'border-b border-[var(--app-separator)]' : ''
}`}
>
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
binding.status === 'converted'
? 'bg-green-500/20'
: binding.status === 'expired'
? 'bg-gray-500/20'
: 'bg-[var(--app-brand-light)]'
}`}>
{binding.status === 'converted' ? (
<CheckCircle className="w-5 h-5 text-green-400" />
) : binding.status === 'expired' ? (
<Clock className="w-5 h-5 text-gray-400" />
) : (
<span className="text-[var(--app-brand)] font-bold">
{(binding.visitorNickname || '用户')[0]}
</span>
)}
</div>
<div>
<p className="text-white text-sm font-medium">
{binding.visitorNickname || binding.visitorPhone || '匿名用户'}
</p>
<p className="text-[var(--app-text-tertiary)] text-xs">
{new Date(binding.bindingTime).toLocaleDateString('zh-CN')}
</p>
</div>
</div>
<div className="text-right">
{binding.status === 'converted' ? (
<>
<p className="text-green-400 font-semibold">+¥{binding.commission?.toFixed(2)}</p>
<p className="text-[var(--app-text-tertiary)] text-xs"> ¥{binding.orderAmount}</p>
</>
) : (
<span className={`text-xs px-2 py-1 rounded-full ${getBindingStatusStyle(binding.daysRemaining)}`}>
{getBindingStatusText(binding)}
</span>
)}
</div>
</div>
))
)}
</div>
</>
)}
</div>
{/* 专属链接 - 简化显示 */}
<div className="glass-card p-5 mb-6">
<div className="flex items-center justify-between mb-3">
<h3 className="text-white font-semibold"></h3>
<span className="text-[var(--app-brand)] text-sm font-mono bg-[var(--app-brand-light)] px-3 py-1.5 rounded-lg">
{user.referralCode}
</span>
</div>
<p className="text-[var(--app-text-tertiary)] text-xs">
<span className="text-[#FFD700]">5%</span><span className="text-[var(--app-brand)]">{distributorShare}%</span>
</p>
</div>
{/* 分享按钮 */}
<div className="space-y-3 mb-6">
<button
onClick={() => setShowPoster(true)}
className="w-full glass-card p-4 flex items-center gap-4 touch-feedback"
>
<div className="w-12 h-12 rounded-xl bg-[var(--ios-indigo)]/20 flex items-center justify-center">
<ImageIcon className="w-6 h-6 text-[var(--ios-indigo)]" />
</div>
<div className="flex-1 text-left">
<p className="text-white font-medium">广</p>
<p className="text-[var(--app-text-tertiary)] text-xs"></p>
</div>
<ArrowRight className="w-5 h-5 text-[var(--app-text-tertiary)]" />
</button>
<button
onClick={handleShareToWechat}
className="w-full glass-card p-4 flex items-center gap-4 touch-feedback"
>
<div className="w-12 h-12 rounded-xl bg-[#07C160]/20 flex items-center justify-center">
<MessageCircle className="w-6 h-6 text-[#07C160]" />
</div>
<div className="flex-1 text-left">
<p className="text-white font-medium"></p>
<p className="text-[var(--app-text-tertiary)] text-xs"></p>
</div>
<ArrowRight className="w-5 h-5 text-[var(--app-text-tertiary)]" />
</button>
<button
onClick={handleShare}
className="w-full glass-card p-4 flex items-center gap-4 touch-feedback"
>
<div className="w-12 h-12 rounded-xl bg-[var(--app-bg-secondary)] flex items-center justify-center">
<Share2 className="w-6 h-6 text-[var(--app-text-secondary)]" />
</div>
<div className="flex-1 text-left">
<p className="text-white font-medium"></p>
<p className="text-[var(--app-text-tertiary)] text-xs">使</p>
</div>
<ArrowRight className="w-5 h-5 text-[var(--app-text-tertiary)]" />
</button>
</div>
{/* 收益明细 */}
{referralPurchases.length > 0 && (
<div className="glass-card overflow-hidden">
<div className="px-5 py-4 border-b border-[var(--app-separator)]">
<h3 className="text-white font-semibold"></h3>
</div>
<div className="max-h-60 overflow-auto scrollbar-hide">
{referralPurchases.slice(0, 10).map((purchase, idx) => (
<div
key={purchase.id}
className={`px-5 py-4 flex items-center justify-between ${idx !== referralPurchases.length - 1 ? 'border-b border-[var(--app-separator)]' : ''}`}
>
<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">
<Gift className="w-5 h-5 text-[var(--app-brand)]" />
</div>
<div>
<p className="text-white text-sm font-medium">
{purchase.type === "fullbook" ? "整本书购买" : "单节购买"}
</p>
<p className="text-[var(--app-text-tertiary)] text-xs">
{new Date(purchase.createdAt).toLocaleDateString("zh-CN")}
</p>
</div>
</div>
<p className="text-[var(--app-brand)] font-semibold">
+¥{(purchase.referrerEarnings || 0).toFixed(2)}
</p>
</div>
))}
</div>
</div>
)}
{/* 空状态 */}
{referralPurchases.length === 0 && activeBindings.length === 0 && (
<div className="glass-card p-8 text-center">
<div className="w-16 h-16 rounded-full bg-[var(--app-bg-secondary)] flex items-center justify-center mx-auto mb-4">
<Gift className="w-8 h-8 text-[var(--app-text-tertiary)]" />
</div>
<p className="text-white font-medium mb-2"></p>
<p className="text-[var(--app-text-tertiary)] text-sm">
{distributorShare}%
</p>
</div>
)}
</main>
<PosterModal
isOpen={showPoster}
onClose={() => setShowPoster(false)}
referralLink={referralLink}
referralCode={user.referralCode}
nickname={user.nickname}
/>
<WithdrawalModal
isOpen={showWithdrawal}
onClose={() => setShowWithdrawal(false)}
availableAmount={totalEarnings}
/>
<AutoWithdrawModal
isOpen={showAutoWithdraw}
onClose={() => setShowAutoWithdraw(false)}
enabled={autoWithdrawEnabled}
threshold={autoWithdrawThreshold}
onSave={(enabled, threshold, account) => {
setAutoWithdrawEnabled(enabled)
setAutoWithdrawThreshold(threshold)
// 实际调用API保存
}}
/>
</div>
)
}

View File

@@ -0,0 +1,276 @@
"use client"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { ChevronLeft, Phone, MessageCircle, CreditCard, Check, X, Loader2, Shield, MapPin } from "lucide-react"
import { useStore } from "@/lib/store"
export default function SettingsPage() {
const router = useRouter()
const { user, updateUser, logout } = useStore()
// 绑定弹窗状态
const [showBindModal, setShowBindModal] = useState(false)
const [bindType, setBindType] = useState<"phone" | "wechat" | "alipay">("phone")
const [bindValue, setBindValue] = useState("")
const [isBinding, setIsBinding] = useState(false)
const [bindError, setBindError] = useState("")
// 绑定账号
const handleBind = async () => {
if (!bindValue.trim()) {
setBindError("请输入内容")
return
}
if (bindType === "phone" && !/^1[3-9]\d{9}$/.test(bindValue)) {
setBindError("请输入正确的手机号")
return
}
if (bindType === "wechat" && bindValue.length < 6) {
setBindError("微信号至少6位")
return
}
if (bindType === "alipay" && !bindValue.includes("@") && !/^1[3-9]\d{9}$/.test(bindValue)) {
setBindError("请输入正确的支付宝账号")
return
}
setIsBinding(true)
setBindError("")
try {
await new Promise(resolve => setTimeout(resolve, 1000))
if (updateUser && user) {
const updates: any = {}
if (bindType === "phone") updates.phone = bindValue
if (bindType === "wechat") updates.wechat = bindValue
if (bindType === "alipay") updates.alipay = bindValue
updateUser(user.id, updates)
}
setShowBindModal(false)
setBindValue("")
alert("绑定成功!")
} catch (error) {
setBindError("绑定失败,请重试")
} finally {
setIsBinding(false)
}
}
const openBindModal = (type: "phone" | "wechat" | "alipay") => {
setBindType(type)
setBindValue("")
setBindError("")
setShowBindModal(true)
}
// 检查是否有绑定任何支付方式
const hasAnyPaymentBound = user?.wechat || user?.alipay
return (
<div className="min-h-screen bg-black text-white pb-24">
{/* Header */}
<header className="sticky top-0 z-40 bg-black/90 backdrop-blur-xl border-b border-white/5">
<div className="px-4 py-3 flex items-center">
<button
onClick={() => router.back()}
className="w-8 h-8 rounded-full bg-white/10 flex items-center justify-center"
>
<ChevronLeft className="w-5 h-5 text-white" />
</button>
<h1 className="flex-1 text-center text-lg font-semibold text-white"></h1>
<div className="w-8" />
</div>
</header>
<main className="px-4 py-4 space-y-4">
{/* 账号绑定 */}
<div className="rounded-2xl bg-[#1c1c1e] border border-white/5 overflow-hidden">
<div className="px-4 py-3 border-b border-white/5">
<div className="flex items-center gap-2">
<Shield className="w-4 h-4 text-[#00CED1]" />
<span className="text-white font-medium"></span>
</div>
<p className="text-white/40 text-xs mt-1"></p>
</div>
{/* 手机号 */}
<button
onClick={() => openBindModal("phone")}
className="w-full flex items-center justify-between p-4 border-b border-white/5 active:bg-white/5"
>
<div className="flex items-center gap-3">
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
user?.phone ? "bg-[#00CED1]/20" : "bg-white/10"
}`}>
<Phone className={`w-4 h-4 ${user?.phone ? "text-[#00CED1]" : "text-white/40"}`} />
</div>
<div className="text-left">
<p className="text-white text-sm"></p>
<p className="text-white/40 text-xs">
{user?.phone || "未绑定"}
</p>
</div>
</div>
{user?.phone ? (
<Check className="w-5 h-5 text-[#00CED1]" />
) : (
<span className="text-[#00CED1] text-xs"></span>
)}
</button>
{/* 微信号 */}
<button
onClick={() => openBindModal("wechat")}
className="w-full flex items-center justify-between p-4 border-b border-white/5 active:bg-white/5"
>
<div className="flex items-center gap-3">
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
user?.wechat ? "bg-[#07C160]/20" : "bg-white/10"
}`}>
<MessageCircle className={`w-4 h-4 ${user?.wechat ? "text-[#07C160]" : "text-white/40"}`} />
</div>
<div className="text-left">
<p className="text-white text-sm"></p>
<p className="text-white/40 text-xs">
{user?.wechat || "未绑定"}
</p>
</div>
</div>
{user?.wechat ? (
<Check className="w-5 h-5 text-[#07C160]" />
) : (
<span className="text-[#07C160] text-xs"></span>
)}
</button>
{/* 支付宝 */}
<button
onClick={() => openBindModal("alipay")}
className="w-full flex items-center justify-between p-4 border-b border-white/5 active:bg-white/5"
>
<div className="flex items-center gap-3">
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
user?.alipay ? "bg-[#1677FF]/20" : "bg-white/10"
}`}>
<CreditCard className={`w-4 h-4 ${user?.alipay ? "text-[#1677FF]" : "text-white/40"}`} />
</div>
<div className="text-left">
<p className="text-white text-sm"></p>
<p className="text-white/40 text-xs">
{user?.alipay || "未绑定"}
</p>
</div>
</div>
{user?.alipay ? (
<Check className="w-5 h-5 text-[#1677FF]" />
) : (
<span className="text-[#1677FF] text-xs"></span>
)}
</button>
{/* 收货地址 */}
<button
onClick={() => router.push("/view/my/addresses")}
className="w-full flex items-center justify-between p-4 active:bg-white/5"
>
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full flex items-center justify-center bg-orange-500/20">
<MapPin className="w-4 h-4 text-orange-400" />
</div>
<div className="text-left">
<p className="text-white text-sm"></p>
<p className="text-white/40 text-xs"></p>
</div>
</div>
<span className="text-[#00CED1] text-xs"></span>
</button>
</div>
{/* 绑定提示 */}
{!hasAnyPaymentBound && (
<div className="p-4 rounded-xl bg-orange-500/10 border border-orange-500/20">
<p className="text-orange-400 text-xs">
使
</p>
</div>
)}
{/* 退出登录 */}
<button
onClick={() => {
logout()
router.push("/view")
}}
className="w-full py-3 rounded-xl bg-[#1c1c1e] text-red-400 font-medium border border-red-400/30"
>
退
</button>
</main>
{/* 绑定弹窗 */}
{showBindModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={() => !isBinding && setShowBindModal(false)} />
<div className="relative w-full max-w-sm bg-[#1c1c1e] rounded-2xl overflow-hidden">
<div className="flex items-center justify-between p-4 border-b border-white/10">
<h3 className="text-lg font-semibold text-white">
{bindType === "phone" ? "手机号" : bindType === "wechat" ? "微信号" : "支付宝"}
</h3>
<button
onClick={() => !isBinding && setShowBindModal(false)}
className="w-8 h-8 rounded-full bg-white/10 flex items-center justify-center"
>
<X className="w-4 h-4 text-white/60" />
</button>
</div>
<div className="p-5">
<div className="mb-4">
<label className="block text-white/40 text-xs mb-2">
{bindType === "phone" ? "手机号" : bindType === "wechat" ? "微信号" : "支付宝账号"}
</label>
<input
type={bindType === "phone" ? "tel" : "text"}
value={bindValue}
onChange={(e) => setBindValue(e.target.value)}
placeholder={
bindType === "phone" ? "请输入11位手机号" :
bindType === "wechat" ? "请输入微信号" :
"请输入支付宝账号"
}
className="w-full px-4 py-3 rounded-xl bg-black/30 border border-white/10 text-white placeholder-white/30 focus:outline-none focus:border-[#00CED1]/50"
disabled={isBinding}
/>
</div>
{bindError && (
<p className="text-red-400 text-sm mb-4">{bindError}</p>
)}
<button
onClick={handleBind}
disabled={isBinding || !bindValue}
className="w-full py-3 rounded-xl bg-[#00CED1] text-black font-medium flex items-center justify-center gap-2 disabled:opacity-50"
>
{isBinding ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
...
</>
) : (
"确认绑定"
)}
</button>
</div>
</div>
</div>
)}
</div>
)
}

221
app/view/page.tsx Normal file
View File

@@ -0,0 +1,221 @@
/**
* 一场SOUL的创业实验 - 首页
* 开发: 卡若
* 技术支持: 存客宝
*/
"use client"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { Search, ChevronRight, BookOpen } from "lucide-react"
import { useStore } from "@/lib/store"
import { bookData, getTotalSectionCount } from "@/lib/book-data"
import { SearchModal } from "@/components/search-modal"
export default function HomePage() {
const router = useRouter()
const { user } = useStore()
const [mounted, setMounted] = useState(false)
const [searchOpen, setSearchOpen] = useState(false)
// 计算数据(必须在所有 hooks 之后)
const totalSections = getTotalSectionCount()
const hasFullBook = user?.hasFullBook || false
const purchasedCount = hasFullBook ? totalSections : user?.purchasedSections?.length || 0
// 推荐章节
const featuredSections = [
{ id: "1.1", title: "荷包:电动车出租的被动收入模式", tag: "免费", part: "真实的人" },
{ id: "3.1", title: "3000万流水如何跑出来", tag: "热门", part: "真实的行业" },
{ id: "8.1", title: "流量杠杆:抖音、Soul、飞书", tag: "推荐", part: "真实的赚钱" },
]
// 最新更新
const latestSection = {
id: "9.14",
title: "大健康私域一个月150万的70后",
part: "真实的赚钱",
}
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return null
}
return (
<div className="min-h-screen bg-black text-white pb-24">
{/* 顶部区域 */}
<header className="px-4 pt-6 pb-4">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-[#00CED1] to-[#20B2AA] flex items-center justify-center shadow-lg shadow-[#00CED1]/30">
<span className="text-white font-bold text-lg">S</span>
</div>
<div>
<h1 className="text-lg font-bold text-white">Soul<span className="text-[#00CED1]"></span></h1>
<p className="text-xs text-gray-500"></p>
</div>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-[#00CED1] bg-[#00CED1]/10 px-2 py-1 rounded-full">{totalSections}</span>
</div>
</div>
{/* 搜索栏 */}
<div
onClick={() => setSearchOpen(true)}
className="flex items-center gap-3 px-4 py-3 rounded-xl bg-[#1c1c1e] border border-white/5 cursor-pointer hover:border-[#00CED1]/30 transition-colors"
>
<Search className="w-4 h-4 text-gray-500" />
<span className="text-gray-500 text-sm">...</span>
</div>
</header>
{/* 搜索弹窗 */}
<SearchModal open={searchOpen} onOpenChange={setSearchOpen} />
<main className="px-4 space-y-5">
{/* Banner卡片 - 最新章节 */}
<div
onClick={() => router.push(`/view/read/${latestSection.id}`)}
className="relative p-5 rounded-2xl overflow-hidden cursor-pointer"
style={{
background: "linear-gradient(135deg, #0d3331 0%, #1a1a2e 50%, #16213e 100%)",
}}
>
<div className="absolute top-0 right-0 w-32 h-32 opacity-20">
<div className="w-full h-full bg-[#00CED1] rounded-full blur-3xl" />
</div>
<span className="inline-block px-2 py-1 rounded text-xs bg-[#00CED1] text-black font-medium mb-3">
</span>
<h2 className="text-lg font-bold text-white mb-2 pr-8">{latestSection.title}</h2>
<p className="text-sm text-gray-400 mb-3">{latestSection.part}</p>
<div className="flex items-center gap-2 text-[#00CED1] text-sm font-medium">
<ChevronRight className="w-4 h-4" />
</div>
</div>
{/* 阅读进度卡 */}
<div className="p-4 rounded-2xl bg-[#1c1c1e] border border-white/5">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-medium text-white"></h3>
<span className="text-xs text-gray-500">
{purchasedCount}/{totalSections}
</span>
</div>
<div className="w-full h-2 bg-[#2c2c2e] rounded-full overflow-hidden mb-3">
<div
className="h-full bg-gradient-to-r from-[#00CED1] to-[#20B2AA] rounded-full transition-all"
style={{ width: `${(purchasedCount / totalSections) * 100}%` }}
/>
</div>
<div className="grid grid-cols-4 gap-3">
<div className="text-center">
<p className="text-[#00CED1] text-lg font-bold">{purchasedCount}</p>
<p className="text-gray-500 text-xs"></p>
</div>
<div className="text-center">
<p className="text-white text-lg font-bold">{totalSections - purchasedCount}</p>
<p className="text-gray-500 text-xs"></p>
</div>
<div className="text-center">
<p className="text-white text-lg font-bold">5</p>
<p className="text-gray-500 text-xs"></p>
</div>
<div className="text-center">
<p className="text-white text-lg font-bold">11</p>
<p className="text-gray-500 text-xs"></p>
</div>
</div>
</div>
{/* 精选推荐 */}
<div>
<div className="flex items-center justify-between mb-3">
<h3 className="text-base font-semibold text-white"></h3>
<button onClick={() => router.push("/view/chapters")} className="text-xs text-[#00CED1] flex items-center gap-1">
<ChevronRight className="w-3 h-3" />
</button>
</div>
<div className="space-y-3">
{featuredSections.map((section) => (
<div
key={section.id}
onClick={() => router.push(`/view/read/${section.id}`)}
className="p-4 rounded-xl bg-[#1c1c1e] border border-white/5 cursor-pointer active:scale-[0.98] transition-transform"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<span className="text-[#00CED1] text-xs font-medium">{section.id}</span>
<span
className={`text-xs px-2 py-0.5 rounded ${
section.tag === "免费"
? "bg-[#00CED1]/10 text-[#00CED1]"
: section.tag === "热门"
? "bg-pink-500/10 text-pink-400"
: "bg-purple-500/10 text-purple-400"
}`}
>
{section.tag}
</span>
</div>
<h4 className="text-white font-medium text-sm mb-1">{section.title}</h4>
<p className="text-gray-500 text-xs">{section.part}</p>
</div>
<ChevronRight className="w-4 h-4 text-gray-600 mt-1" />
</div>
</div>
))}
</div>
</div>
<div>
<h3 className="text-base font-semibold text-white mb-3"></h3>
<div className="space-y-3">
{bookData.map((part) => (
<div
key={part.id}
onClick={() => router.push("/view/chapters")}
className="p-4 rounded-xl bg-[#1c1c1e] border border-white/5 cursor-pointer active:scale-[0.98] transition-transform"
>
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-[#00CED1]/20 to-[#20B2AA]/10 flex items-center justify-center shrink-0">
<span className="text-[#00CED1] font-bold text-sm">{part.number}</span>
</div>
<div className="flex-1 min-w-0">
<h4 className="text-white font-medium text-sm mb-0.5">{part.title}</h4>
<p className="text-gray-500 text-xs truncate">{part.subtitle}</p>
</div>
<ChevronRight className="w-4 h-4 text-gray-600 shrink-0" />
</div>
</div>
))}
</div>
</div>
{/* 序言入口 */}
<div
onClick={() => router.push("/view/read/preface")}
className="p-4 rounded-xl bg-gradient-to-r from-[#00CED1]/10 to-transparent border border-[#00CED1]/20 cursor-pointer"
>
<div className="flex items-center justify-between">
<div>
<h4 className="text-white font-medium text-sm mb-1"></h4>
<p className="text-gray-400 text-xs">6Soul开播?</p>
</div>
<span className="text-xs text-[#00CED1] bg-[#00CED1]/10 px-2 py-1 rounded"></span>
</div>
</div>
</main>
{/* 使用统一的底部导航组件 */}
</div>
)
}

View File

@@ -0,0 +1,92 @@
import { notFound } from "next/navigation"
import { ChapterContent } from "@/components/chapter-content"
import { getSectionBySlug, getChapterBySectionSlug } from "@/lib/book-file-system"
import { specialSections, getSectionById } from "@/lib/book-data"
import { query } from "@/lib/db"
interface ReadPageProps {
params: Promise<{ id: string }>
}
export const dynamic = "force-dynamic"
export const runtime = "nodejs"
// 从数据库获取章节数据(包含最新的 isFree 状态)
async function getChapterFromDB(id: string) {
try {
const results = await query(
`SELECT id, part_title, chapter_title, section_title, content, is_free, price
FROM chapters
WHERE id = ? AND status = 'published'`,
[id]
) as any[]
if (results && results.length > 0) {
const chapter = results[0]
return {
id: chapter.id,
title: chapter.section_title,
price: chapter.price || 1,
isFree: chapter.is_free === 1 || chapter.price === 0,
filePath: '',
content: chapter.content,
partTitle: chapter.part_title,
chapterTitle: chapter.chapter_title,
}
}
} catch (error) {
console.error("[ReadPage] 从数据库获取章节失败:", error)
}
return null
}
export default async function ReadPage({ params }: ReadPageProps) {
const { id } = await params
if (id === "preface") {
return <ChapterContent section={specialSections.preface as any} partTitle="序言" chapterTitle="" />
}
if (id === "epilogue") {
return <ChapterContent section={specialSections.epilogue as any} partTitle="尾声" chapterTitle="" />
}
if (id.startsWith("appendix-")) {
const appendixSection = specialSections.appendix.find((a) => a.id === id)
if (appendixSection) {
return <ChapterContent section={appendixSection as any} partTitle="附录" chapterTitle="" />
}
}
try {
// 🔥 优先从数据库获取(包含最新的 isFree 状态)
const dbChapter = await getChapterFromDB(id)
if (dbChapter) {
return <ChapterContent
section={dbChapter as any}
partTitle={dbChapter.partTitle || ""}
chapterTitle={dbChapter.chapterTitle || ""}
/>
}
// 如果数据库没有,再从文件系统获取(兼容旧数据)
const section = getSectionBySlug(id)
if (section) {
const context = getChapterBySectionSlug(id)
if (context) {
return <ChapterContent section={section} partTitle={context.part.title} chapterTitle={context.chapter.title} />
}
}
// 最后从 book-data 获取
const bookSection = getSectionById(id)
if (bookSection) {
return <ChapterContent section={bookSection as any} partTitle="" chapterTitle="" />
}
notFound()
} catch (error) {
console.error("[Karuo] Error in ReadPage:", error)
notFound()
}
}

144
app/view/temp_page.tsx Normal file
View File

@@ -0,0 +1,144 @@
import { BookCover } from "@/components/book-cover"
import { BookIntro } from "@/components/book-intro"
import { PurchaseSection } from "@/components/purchase-section"
import { Footer } from "@/components/footer"
import { MatchSection } from "@/components/match-section"
import { getBookStructure } from "@/lib/book-file-system"
import { Home, Sparkles, User } from "lucide-react"
import Link from "next/link"
export default async function HomePage() {
const parts = getBookStructure()
const totalChapters = parts.reduce((acc, part) => acc + part.chapters.length, 0)
const totalSections = parts.reduce((acc, part) => {
return acc + part.chapters.reduce((sum, ch) => sum + ch.sections.length, 0)
}, 0)
return (
<main className="min-h-screen bg-black text-white pb-24 relative overflow-x-hidden">
{/* 顶部标签 */}
<div className="flex justify-center pt-8 mb-6">
<div className="glass-card px-4 py-1.5 flex items-center gap-2 border-[0.5px] border-white/10 rounded-full">
<Sparkles className="w-3.5 h-3.5 text-[#30D158]" />
<span className="text-xs font-medium text-white/80 tracking-wider">Soul · </span>
</div>
</div>
{/* 核心标题区 */}
<div className="px-8 text-center mb-10">
<h1 className="text-[40px] font-bold leading-tight mb-4 tracking-tight">
SOUL的<br />
<span className="text-[#30D158]"></span>
</h1>
<p className="text-white/60 text-lg mb-4">Soul派对房的真实商业故事</p>
<p className="text-[#30D158]/80 italic text-sm"></p>
</div>
{/* 核心数据卡片 */}
<div className="px-6 mb-10">
<div className="glass-card grid grid-cols-2 p-6 divide-x divide-white/10">
<div className="text-center">
<div className="text-2xl font-bold text-[#30D158] mb-1">¥9.9</div>
<div className="text-[10px] text-white/40"></div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-white mb-1">{totalSections}</div>
<div className="text-[10px] text-white/40"></div>
</div>
</div>
</div>
{/* 作者卡片 */}
<div className="px-6 mb-10">
<div className="flex items-center justify-between px-2">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-[#30D158] flex items-center justify-center text-black font-bold text-sm"></div>
<div>
<div className="text-xs text-white/40 mb-0.5"></div>
<div className="text-sm font-medium"></div>
</div>
</div>
<div className="text-right">
<div className="text-xs text-white/40 mb-0.5"></div>
<div className="text-sm font-medium text-[#30D158]">06:00-09:00</div>
</div>
</div>
</div>
{/* 立即阅读按钮 */}
<div className="px-6 mb-6">
<Link href="/view/read/preface" className="btn-ios w-full py-4 text-lg shadow-[0_0_20px_rgba(48,209,88,0.2)]">
<div className="flex items-center gap-2">
<Home className="w-5 h-5" />
<span></span>
</div>
</Link>
<p className="text-center text-[10px] text-white/30 mt-3"> · 3</p>
</div>
{/* 引用寄语 */}
<div className="px-6 mb-10">
<div className="glass-card p-8 relative overflow-hidden">
<div className="absolute top-0 left-0 w-1 h-full bg-[#30D158]"></div>
<div className="text-[#30D158] mb-4">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M14.017 21L14.017 18C14.017 16.8954 14.9124 16 16.017 16H19.017C20.1216 16 21.017 16.8954 21.017 18V21C21.017 22.1046 20.1216 23 19.017 23H16.017C14.9124 23 14.017 22.1046 14.017 21ZM14.017 21C14.017 19.8954 13.1216 19 12.017 19H9.017C7.91243 19 7.017 19.8954 7.017 21V23C7.017 22.1046 7.91243 23 9.017 23H12.017C13.1216 23 14.017 22.1046 14.017 21ZM5.017 21V18C5.017 16.8954 5.91243 16 7.017 16H10.017C11.1216 16 12.017 16.8954 12.017 18V21C12.017 22.1046 11.1216 23 10.017 23H7.017C5.91243 23 5.017 22.1046 5.017 21Z" />
</svg>
</div>
<p className="text-white/80 leading-relaxed text-sm mb-6">
69Soul派对房和几百个陌生人分享的真实故事
</p>
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-white/10 flex items-center justify-center text-[10px]"></div>
<div>
<div className="text-xs font-medium"></div>
<div className="text-[10px] text-white/30">Soul派对房主理人</div>
</div>
</div>
</div>
</div>
{/* 核心亮点数据 */}
<div className="px-6 mb-16">
<div className="grid grid-cols-3 gap-4">
<div className="text-center">
<div className="text-xl font-bold mb-1 tracking-tight">55+</div>
<div className="text-[10px] text-white/40"></div>
</div>
<div className="text-center">
<div className="text-xl font-bold mb-1 tracking-tight">11</div>
<div className="text-[10px] text-white/40"></div>
</div>
<div className="text-center">
<div className="text-xl font-bold mb-1 tracking-tight">100+</div>
<div className="text-[10px] text-white/40"></div>
</div>
</div>
</div>
{/* 购买区域 */}
<PurchaseSection />
{/* 匹配区域预览 */}
<MatchSection />
<Footer />
{/* 底部导航 */}
<nav className="fixed bottom-0 left-0 right-0 h-20 bg-black/80 backdrop-blur-xl border-t border-white/5 flex items-center justify-around px-6 z-50">
<Link href="/view" className="flex flex-col items-center gap-1 text-[#30D158]">
<Home className="w-6 h-6" />
<span className="text-[10px] font-medium"></span>
</Link>
<Link href="/view/match" className="flex flex-col items-center gap-1 text-white/40">
<Sparkles className="w-6 h-6" />
<span className="text-[10px] font-medium"></span>
</Link>
<Link href="/view/my" className="flex flex-col items-center gap-1 text-white/40">
<User className="w-6 h-6" />
<span className="text-[10px] font-medium"></span>
</Link>
</nav>
</main>
)
}