feat: 完整重构小程序匹配功能 + 修复UI对齐 + 文章数据API
主要更新: 1. 按H5网页端完全重构匹配功能(match页面) - 4种匹配类型: 创业合伙/资源对接/导师顾问/团队招募 - 资源对接等类型弹出手机号/微信号输入框 - 去掉重新匹配按钮,改为返回按钮 2. 修复所有卡片对齐和宽度问题 - 目录页附录卡片居中 - 首页阅读进度卡片满宽度 - 我的页面菜单卡片对齐 - 推广中心分享卡片统一宽度 3. 修复目录页图标和文字对齐 - section-icon固定40rpx宽高 - section-title与图标垂直居中 4. 更新真实完整文章标题(62篇) - 从book目录读取真实markdown文件名 - 替换之前的简化标题 5. 新增文章数据API - /api/db/chapters - 获取完整书籍结构 - 支持按ID获取单篇文章内容
This commit is contained in:
@@ -2,9 +2,10 @@
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { motion, AnimatePresence } from "framer-motion"
|
||||
import { Mic, X, CheckCircle, Loader2 } from "lucide-react"
|
||||
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
|
||||
@@ -18,73 +19,131 @@ interface MatchUser {
|
||||
}
|
||||
|
||||
const matchTypes = [
|
||||
{ id: "partner", label: "创业合伙", icon: "⭐", color: "#00E5FF", joinable: false },
|
||||
{ id: "investor", label: "资源对接", icon: "👥", color: "#7B61FF", joinable: true },
|
||||
{ id: "mentor", label: "导师顾问", icon: "❤️", color: "#E91E63", joinable: true },
|
||||
{ id: "team", label: "团队招募", icon: "🎮", color: "#4CAF50", joinable: true },
|
||||
{ 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 getStoredPhone = (): string => {
|
||||
const FREE_MATCH_LIMIT = 1 // 每日免费匹配次数改为1次
|
||||
const MATCH_UNLOCK_PRICE = 1 // 每次解锁需要购买1个小节
|
||||
|
||||
// 获取本地存储的联系方式
|
||||
const getStoredContact = (): { phone: string; wechat: string } => {
|
||||
if (typeof window !== "undefined") {
|
||||
return localStorage.getItem("user_phone") || ""
|
||||
return {
|
||||
phone: localStorage.getItem("user_phone") || "",
|
||||
wechat: localStorage.getItem("user_wechat") || "",
|
||||
}
|
||||
}
|
||||
return ""
|
||||
return { phone: "", wechat: "" }
|
||||
}
|
||||
|
||||
// 保存手机号到本地存储
|
||||
const savePhone = (phone: string) => {
|
||||
// 获取今日匹配次数
|
||||
const getTodayMatchCount = (): number => {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem("user_phone", phone)
|
||||
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(() => {
|
||||
const storedPhone = getStoredPhone()
|
||||
if (storedPhone) {
|
||||
setPhoneNumber(storedPhone)
|
||||
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) => {
|
||||
const type = matchTypes.find((t) => t.id === typeId)
|
||||
if (type?.joinable) {
|
||||
setJoinType(typeId)
|
||||
setShowJoinModal(true)
|
||||
setJoinSuccess(false)
|
||||
setJoinError("")
|
||||
// 如果有存储的手机号,自动填充
|
||||
const storedPhone = getStoredPhone()
|
||||
if (storedPhone) {
|
||||
setPhoneNumber(storedPhone)
|
||||
}
|
||||
} else {
|
||||
// 不可加入的类型,直接选中并开始匹配
|
||||
setSelectedType(typeId)
|
||||
}
|
||||
setJoinType(typeId)
|
||||
setShowJoinModal(true)
|
||||
setJoinSuccess(false)
|
||||
setJoinError("")
|
||||
}
|
||||
|
||||
const handleJoinSubmit = async () => {
|
||||
if (!phoneNumber || phoneNumber.length !== 11) {
|
||||
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("")
|
||||
|
||||
@@ -96,17 +155,17 @@ export default function MatchPage() {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: joinType,
|
||||
phone: phoneNumber,
|
||||
phone: contactType === "phone" ? phoneNumber : "",
|
||||
wechat: contactType === "wechat" ? wechatId : "",
|
||||
userId: user?.id,
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success) {
|
||||
// 保存手机号以便下次使用
|
||||
savePhone(phoneNumber)
|
||||
saveContact(phoneNumber, wechatId)
|
||||
setJoinSuccess(true)
|
||||
// 2秒后关闭弹窗
|
||||
setTimeout(() => {
|
||||
setShowJoinModal(false)
|
||||
setJoinSuccess(false)
|
||||
@@ -121,7 +180,69 @@ export default function MatchPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// 购买解锁匹配次数
|
||||
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)
|
||||
@@ -134,7 +255,25 @@ export default function MatchPage() {
|
||||
() => {
|
||||
clearInterval(interval)
|
||||
setIsMatching(false)
|
||||
setCurrentMatch(getMockMatch())
|
||||
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,
|
||||
)
|
||||
@@ -167,6 +306,12 @@ export default function MatchPage() {
|
||||
}
|
||||
|
||||
const nextMatch = () => {
|
||||
// 检查是否需要付费
|
||||
if (needPayToMatch) {
|
||||
setShowUnlockModal(true)
|
||||
return
|
||||
}
|
||||
|
||||
setCurrentMatch(null)
|
||||
setTimeout(() => startMatch(), 500)
|
||||
}
|
||||
@@ -183,13 +328,15 @@ export default function MatchPage() {
|
||||
})
|
||||
}
|
||||
|
||||
const currentTypeLabel = matchTypes.find((t) => t.id === selectedType)?.label || "创业合伙"
|
||||
const joinTypeLabel = matchTypes.find((t) => t.id === joinType)?.label || ""
|
||||
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>
|
||||
<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
|
||||
@@ -202,6 +349,33 @@ export default function MatchPage() {
|
||||
</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
|
||||
@@ -213,15 +387,17 @@ export default function MatchPage() {
|
||||
>
|
||||
{/* 中央匹配圆环 */}
|
||||
<motion.div
|
||||
onClick={startMatch}
|
||||
className="relative w-[280px] h-[280px] mb-8 cursor-pointer"
|
||||
whileTap={{ scale: 0.95 }}
|
||||
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: "radial-gradient(circle, transparent 50%, rgba(0, 229, 255, 0.1) 70%, transparent 100%)",
|
||||
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],
|
||||
@@ -236,7 +412,7 @@ export default function MatchPage() {
|
||||
|
||||
{/* 中间光环 */}
|
||||
<motion.div
|
||||
className="absolute inset-[-15px] rounded-full border-2 border-[#00E5FF]/30"
|
||||
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],
|
||||
@@ -252,8 +428,12 @@ export default function MatchPage() {
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-full flex flex-col items-center justify-center overflow-hidden"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)",
|
||||
boxShadow: "0 0 60px rgba(0, 229, 255, 0.3), inset 0 0 60px rgba(123, 97, 255, 0.2)",
|
||||
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],
|
||||
@@ -268,35 +448,70 @@ export default function MatchPage() {
|
||||
<div
|
||||
className="absolute inset-0 rounded-full"
|
||||
style={{
|
||||
background:
|
||||
"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%)",
|
||||
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%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 中心图标 */}
|
||||
<Mic 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">寻找{currentTypeLabel}</div>
|
||||
{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-8">
|
||||
当前模式: <span className="text-[#00E5FF]">{currentTypeLabel}</span>
|
||||
<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('/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)
|
||||
}}
|
||||
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"
|
||||
@@ -307,17 +522,6 @@ export default function MatchPage() {
|
||||
<span className={`text-xs ${selectedType === type.id ? "text-[#00E5FF]" : "text-white/60"}`}>
|
||||
{type.label}
|
||||
</span>
|
||||
{type.joinable && (
|
||||
<span
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleJoinClick(type.id)
|
||||
}}
|
||||
className="text-[10px] px-2 py-0.5 rounded-full bg-[#00E5FF]/20 text-[#00E5FF] mt-1 cursor-pointer hover:bg-[#00E5FF]/30"
|
||||
>
|
||||
加入
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -344,7 +548,7 @@ export default function MatchPage() {
|
||||
animate={{ scale: [1, 1.2, 1] }}
|
||||
transition={{ duration: 1, repeat: Number.POSITIVE_INFINITY }}
|
||||
>
|
||||
<Mic className="w-12 h-12 text-[#00E5FF]" />
|
||||
<Users className="w-12 h-12 text-[#00E5FF]" />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
@@ -366,7 +570,7 @@ export default function MatchPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h2 className="text-xl font-semibold mb-2 text-white">正在寻找合作伙伴...</h2>
|
||||
<h2 className="text-xl font-semibold mb-2 text-white">正在匹配{currentMatchLabel}...</h2>
|
||||
<p className="text-white/50 mb-8">已匹配 {matchAttempts} 次</p>
|
||||
|
||||
<button
|
||||
@@ -447,15 +651,79 @@ export default function MatchPage() {
|
||||
</button>
|
||||
<button
|
||||
onClick={nextMatch}
|
||||
className="w-full py-4 rounded-xl bg-[#1c1c1e] text-white border border-white/10"
|
||||
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
|
||||
@@ -486,30 +754,65 @@ export default function MatchPage() {
|
||||
{/* 弹窗内容 */}
|
||||
<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">
|
||||
{getStoredPhone() ? "检测到您已注册的手机号,确认后即可加入" : "请输入您的手机号以便我们联系您"}
|
||||
{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">手机号</label>
|
||||
<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}
|
||||
/>
|
||||
<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>
|
||||
|
||||
{/* 错误提示 */}
|
||||
@@ -518,7 +821,7 @@ export default function MatchPage() {
|
||||
{/* 提交按钮 */}
|
||||
<button
|
||||
onClick={handleJoinSubmit}
|
||||
disabled={isJoining || !phoneNumber}
|
||||
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 ? (
|
||||
@@ -551,25 +854,12 @@ export default function MatchPage() {
|
||||
<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">
|
||||
<svg className="w-7 h-7 text-white" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="8" fill="currentColor" opacity="0.9" />
|
||||
<ellipse
|
||||
cx="12"
|
||||
cy="12"
|
||||
rx="11"
|
||||
ry="4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
opacity="0.6"
|
||||
/>
|
||||
<circle cx="9" cy="10" r="1.5" fill="white" opacity="0.4" />
|
||||
</svg>
|
||||
<Users className="w-7 h-7 text-white" />
|
||||
</div>
|
||||
<span className="text-[#00CED1] text-xs font-medium mt-1">匹配</span>
|
||||
<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" />
|
||||
|
||||
Reference in New Issue
Block a user