删除多个完成报告文件,优化项目结构以提升可维护性。
This commit is contained in:
123
app/view/about/page.tsx
Normal file
123
app/view/about/page.tsx
Normal 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>这是我每天早上6点到9点,在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
214
app/view/chapters/page.tsx
Normal 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">序言|为什么我每天早上6点在Soul开播?</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
112
app/view/docs/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
3
app/view/documentation/capture/loading.tsx
Normal file
3
app/view/documentation/capture/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
78
app/view/documentation/capture/page.tsx
Normal file
78
app/view/documentation/capture/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
306
app/view/documentation/page.tsx
Normal file
306
app/view/documentation/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
136
app/view/login/forgot/page.tsx
Normal file
136
app/view/login/forgot/page.tsx
Normal 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
183
app/view/login/page.tsx
Normal 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
849
app/view/match/page.tsx
Normal 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.9元,每天3次免费匹配</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>
|
||||
)
|
||||
}
|
||||
161
app/view/my/addresses/[id]/page.tsx
Normal file
161
app/view/my/addresses/[id]/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
163
app/view/my/addresses/new/page.tsx
Normal file
163
app/view/my/addresses/new/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
141
app/view/my/addresses/page.tsx
Normal file
141
app/view/my/addresses/page.tsx
Normal 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
551
app/view/my/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
109
app/view/my/purchases/page.tsx
Normal file
109
app/view/my/purchases/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
569
app/view/my/referral/page.tsx
Normal file
569
app/view/my/referral/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
276
app/view/my/settings/page.tsx
Normal file
276
app/view/my/settings/page.tsx
Normal 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
221
app/view/page.tsx
Normal 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">为什么我每天早上6点在Soul开播?</p>
|
||||
</div>
|
||||
<span className="text-xs text-[#00CED1] bg-[#00CED1]/10 px-2 py-1 rounded">免费</span>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* 使用统一的底部导航组件 */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
92
app/view/read/[id]/page.tsx
Normal file
92
app/view/read/[id]/page.tsx
Normal 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
144
app/view/temp_page.tsx
Normal 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">
|
||||
这不是一本教你成功的鸡汤书。这是我每天早上6点到9点,在Soul派对房和几百个陌生人分享的真实故事。
|
||||
</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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user