Files
soul/app/match/page.tsx
卡若 b60edb3d47 feat: 完整重构小程序匹配功能 + 修复UI对齐 + 文章数据API
主要更新:
1. 按H5网页端完全重构匹配功能(match页面)
   - 4种匹配类型: 创业合伙/资源对接/导师顾问/团队招募
   - 资源对接等类型弹出手机号/微信号输入框
   - 去掉重新匹配按钮,改为返回按钮

2. 修复所有卡片对齐和宽度问题
   - 目录页附录卡片居中
   - 首页阅读进度卡片满宽度
   - 我的页面菜单卡片对齐
   - 推广中心分享卡片统一宽度

3. 修复目录页图标和文字对齐
   - section-icon固定40rpx宽高
   - section-title与图标垂直居中

4. 更新真实完整文章标题(62篇)
   - 从book目录读取真实markdown文件名
   - 替换之前的简化标题

5. 新增文章数据API
   - /api/db/chapters - 获取完整书籍结构
   - 支持按ID获取单篇文章内容
2026-01-21 15:49:12 +08:00

874 lines
35 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

"use client"
import { useState, useEffect } from "react"
import { motion, AnimatePresence } from "framer-motion"
import { Users, X, CheckCircle, Loader2, Lock, Zap, Gift } from "lucide-react"
import { Home, List, User } 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])
if (!mounted) return null // 彻底解决 Hydration 错误
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 || ""
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('/chapters')}
className="px-3 py-1.5 rounded-full bg-[#FFD700]/20 text-[#FFD700] text-xs font-medium"
>
+1
</button>
)}
</div>
</div>
</div>
)}
<AnimatePresence mode="wait">
{!isMatching && !currentMatch && (
<motion.div
key="idle"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="flex flex-col items-center px-6"
>
{/* 中央匹配圆环 */}
<motion.div
onClick={hasPurchased ? startMatch : undefined}
className={`relative w-[280px] h-[280px] mb-8 ${hasPurchased ? 'cursor-pointer' : 'cursor-not-allowed'}`}
whileTap={hasPurchased ? { scale: 0.95 } : undefined}
>
{/* 外层光环 */}
<motion.div
className="absolute inset-[-30px] rounded-full"
style={{
background: hasPurchased
? "radial-gradient(circle, transparent 50%, rgba(0, 229, 255, 0.1) 70%, transparent 100%)"
: "radial-gradient(circle, transparent 50%, rgba(100, 100, 100, 0.1) 70%, transparent 100%)",
}}
animate={{
scale: [1, 1.1, 1],
opacity: [0.5, 0.8, 0.5],
}}
transition={{
duration: 2,
repeat: Number.POSITIVE_INFINITY,
ease: "easeInOut",
}}
/>
{/* 中间光环 */}
<motion.div
className={`absolute inset-[-15px] rounded-full border-2 ${hasPurchased ? 'border-[#00E5FF]/30' : 'border-gray-600/30'}`}
animate={{
scale: [1, 1.05, 1],
opacity: [0.3, 0.6, 0.3],
}}
transition={{
duration: 1.5,
repeat: Number.POSITIVE_INFINITY,
ease: "easeInOut",
}}
/>
{/* 内层渐变球 */}
<motion.div
className="absolute inset-0 rounded-full flex flex-col items-center justify-center overflow-hidden"
style={{
background: hasPurchased
? "linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)"
: "linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 50%, #1a1a1a 100%)",
boxShadow: hasPurchased
? "0 0 60px rgba(0, 229, 255, 0.3), inset 0 0 60px rgba(123, 97, 255, 0.2)"
: "0 0 30px rgba(100, 100, 100, 0.2)",
}}
animate={{
y: [0, -5, 0],
}}
transition={{
duration: 3,
repeat: Number.POSITIVE_INFINITY,
ease: "easeInOut",
}}
>
{/* 内部渐变光效 */}
<div
className="absolute inset-0 rounded-full"
style={{
background: hasPurchased
? "radial-gradient(circle at 30% 30%, rgba(123, 97, 255, 0.4) 0%, transparent 50%), radial-gradient(circle at 70% 70%, rgba(233, 30, 99, 0.3) 0%, transparent 50%)"
: "radial-gradient(circle at 30% 30%, rgba(100, 100, 100, 0.2) 0%, transparent 50%)",
}}
/>
{/* 中心图标 */}
{hasPurchased ? (
needPayToMatch ? (
<>
<Zap className="w-12 h-12 text-[#FFD700] mb-3 relative z-10" />
<div className="text-xl font-bold text-white mb-1 relative z-10"></div>
<div className="text-sm text-white/60 relative z-10"></div>
</>
) : (
<>
<Users className="w-12 h-12 text-white/90 mb-3 relative z-10" />
<div className="text-xl font-bold text-white mb-1 relative z-10"></div>
<div className="text-sm text-white/60 relative z-10">{currentMatchLabel}</div>
</>
)
) : (
<>
<Lock className="w-12 h-12 text-gray-500 mb-3 relative z-10" />
<div className="text-xl font-bold text-gray-400 mb-1 relative z-10"></div>
<div className="text-sm text-gray-500 relative z-10">9.9使</div>
</>
)}
</motion.div>
</motion.div>
{/* 当前模式显示 */}
<p className="text-white/50 text-sm mb-4">
: <span className={hasPurchased ? "text-[#00E5FF]" : "text-gray-500"}>{currentTypeLabel}</span>
</p>
{/* 购买提示 */}
{!hasPurchased && (
<div className="w-full mb-6 p-4 rounded-xl bg-gradient-to-r from-[#00E5FF]/10 to-transparent border border-[#00E5FF]/20">
<div className="flex items-center justify-between">
<div>
<p className="text-white font-medium"></p>
<p className="text-gray-400 text-sm mt-1">9.93</p>
</div>
<button
onClick={() => router.push('/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('/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>
<nav className="fixed bottom-0 left-0 right-0 bg-[#1c1c1e]/95 backdrop-blur-xl border-t border-white/5 pb-safe-bottom">
<div className="px-4 py-2">
<div className="flex items-center justify-around">
<button onClick={() => router.push("/")} className="flex flex-col items-center py-2 px-4">
<Home className="w-5 h-5 text-gray-500 mb-1" />
<span className="text-gray-500 text-xs"></span>
</button>
<button onClick={() => router.push("/chapters")} className="flex flex-col items-center py-2 px-4">
<List className="w-5 h-5 text-gray-500 mb-1" />
<span className="text-gray-500 text-xs"></span>
</button>
{/* 找伙伴按钮 - 当前页面高亮 */}
<button className="flex flex-col items-center py-2 px-6 -mt-4">
<div className="w-14 h-14 rounded-full bg-gradient-to-br from-[#00CED1] to-[#20B2AA] flex items-center justify-center shadow-lg shadow-[#00CED1]/30">
<Users className="w-7 h-7 text-white" />
</div>
<span className="text-[#00CED1] text-xs font-medium mt-1"></span>
</button>
<button onClick={() => router.push("/my")} className="flex flex-col items-center py-2 px-4">
<User className="w-5 h-5 text-gray-500 mb-1" />
<span className="text-gray-500 text-xs"></span>
</button>
</div>
</div>
</nav>
</div>
)
}