feat: implement CKB API integration
Add CKB API routes and update match page for joining features #VERCEL_SKIP Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
86
app/api/ckb/join/route.ts
Normal file
86
app/api/ckb/join/route.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { type NextRequest, NextResponse } from "next/server"
|
||||||
|
import crypto from "crypto"
|
||||||
|
|
||||||
|
// 存客宝API配置
|
||||||
|
const CKB_API_KEY = "fyngh-ecy9h-qkdae-epwd5-rz6kd"
|
||||||
|
const CKB_API_URL = "https://ckbapi.quwanzhi.com/v1/api/scenarios"
|
||||||
|
|
||||||
|
// 生成签名
|
||||||
|
function generateSign(apiKey: string, timestamp: number): string {
|
||||||
|
const signStr = `${apiKey}${timestamp}`
|
||||||
|
return crypto.createHash("md5").update(signStr).digest("hex")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不同类型对应的source标签
|
||||||
|
const sourceMap: Record<string, string> = {
|
||||||
|
team: "团队招募",
|
||||||
|
investor: "资源对接",
|
||||||
|
mentor: "导师顾问",
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagsMap: Record<string, string> = {
|
||||||
|
team: "切片团队,团队招募",
|
||||||
|
investor: "资源对接,资源群",
|
||||||
|
mentor: "导师顾问,咨询服务",
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json()
|
||||||
|
const { type, phone, name, wechatId, remark } = body
|
||||||
|
|
||||||
|
// 验证必填参数
|
||||||
|
if (!type || !phone) {
|
||||||
|
return NextResponse.json({ success: false, message: "缺少必填参数" }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证类型
|
||||||
|
if (!["team", "investor", "mentor"].includes(type)) {
|
||||||
|
return NextResponse.json({ success: false, message: "无效的加入类型" }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成时间戳和签名
|
||||||
|
const timestamp = Math.floor(Date.now() / 1000)
|
||||||
|
const sign = generateSign(CKB_API_KEY, timestamp)
|
||||||
|
|
||||||
|
// 构建请求参数
|
||||||
|
const requestBody = {
|
||||||
|
apiKey: CKB_API_KEY,
|
||||||
|
sign,
|
||||||
|
timestamp,
|
||||||
|
phone,
|
||||||
|
name: name || "",
|
||||||
|
wechatId: wechatId || "",
|
||||||
|
source: sourceMap[type],
|
||||||
|
remark: remark || `来自创业实验APP-${sourceMap[type]}`,
|
||||||
|
tags: tagsMap[type],
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用存客宝API
|
||||||
|
const response = await fetch(CKB_API_URL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestBody),
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
|
||||||
|
if (response.ok && result.code === 0) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: `成功加入${sourceMap[type]}`,
|
||||||
|
data: result.data,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
message: result.message || "加入失败,请稍后重试",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("存客宝API调用失败:", error)
|
||||||
|
return NextResponse.json({ success: false, message: "服务器错误,请稍后重试" }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
import { type NextRequest, NextResponse } from "next/server"
|
|
||||||
import crypto from "crypto"
|
|
||||||
|
|
||||||
// 存客宝API配置
|
|
||||||
const CKB_CONFIG = {
|
|
||||||
apiKey: "fyngh-ecy9h-qkdae-epwd5-rz6kd",
|
|
||||||
apiUrl: "https://ckbapi.quwanzhi.com/v1/api/scenarios",
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成签名
|
|
||||||
function generateSign(apiKey: string, timestamp: number): string {
|
|
||||||
const signStr = `${apiKey}${timestamp}`
|
|
||||||
return crypto.createHash("md5").update(signStr).digest("hex")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 不同场景的source和tags配置
|
|
||||||
const SCENARIO_CONFIG: Record<string, { source: string; tags: string }> = {
|
|
||||||
team: {
|
|
||||||
source: "卡若创业实验-切片团队招募",
|
|
||||||
tags: "切片团队,团队招募,创业合作",
|
|
||||||
},
|
|
||||||
resource: {
|
|
||||||
source: "卡若创业实验-资源对接",
|
|
||||||
tags: "资源对接,资源群,商业合作",
|
|
||||||
},
|
|
||||||
mentor: {
|
|
||||||
source: "卡若创业实验-导师顾问",
|
|
||||||
tags: "导师顾问,创业指导,商业咨询",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const body = await request.json()
|
|
||||||
const { phone, name, scenario, remark } = body
|
|
||||||
|
|
||||||
// 验证必填参数
|
|
||||||
if (!phone) {
|
|
||||||
return NextResponse.json({ success: false, message: "手机号不能为空" }, { status: 400 })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!scenario || !SCENARIO_CONFIG[scenario]) {
|
|
||||||
return NextResponse.json({ success: false, message: "无效的场景类型" }, { status: 400 })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成时间戳和签名
|
|
||||||
const timestamp = Math.floor(Date.now() / 1000)
|
|
||||||
const sign = generateSign(CKB_CONFIG.apiKey, timestamp)
|
|
||||||
|
|
||||||
// 获取场景配置
|
|
||||||
const config = SCENARIO_CONFIG[scenario]
|
|
||||||
|
|
||||||
// 构建请求体
|
|
||||||
const requestBody = {
|
|
||||||
apiKey: CKB_CONFIG.apiKey,
|
|
||||||
sign,
|
|
||||||
timestamp,
|
|
||||||
phone,
|
|
||||||
name: name || "",
|
|
||||||
source: config.source,
|
|
||||||
remark: remark || `来自${config.source}`,
|
|
||||||
tags: config.tags,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用存客宝API
|
|
||||||
const response = await fetch(CKB_CONFIG.apiUrl, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(requestBody),
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await response.json()
|
|
||||||
|
|
||||||
if (response.ok && result.code === 0) {
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
message: "提交成功,我们会尽快与您联系",
|
|
||||||
data: result.data,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
message: result.message || "提交失败,请稍后重试",
|
|
||||||
},
|
|
||||||
{ status: 400 },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("存客宝API调用失败:", error)
|
|
||||||
return NextResponse.json({ success: false, message: "服务器错误,请稍后重试" }, { status: 500 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import { motion, AnimatePresence } from "framer-motion"
|
import { motion, AnimatePresence } from "framer-motion"
|
||||||
import { Mic, X } from "lucide-react"
|
import { Mic, X, CheckCircle, Loader2 } from "lucide-react"
|
||||||
import { Home, List, User } from "lucide-react"
|
import { Home, List, User } from "lucide-react"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
|
|
||||||
@@ -18,208 +18,109 @@ interface MatchUser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const matchTypes = [
|
const matchTypes = [
|
||||||
{ id: "team", label: "团队招募", icon: "🎮", color: "#4CAF50", scenario: "team" },
|
{ id: "partner", label: "创业合伙", icon: "⭐", color: "#00E5FF", joinable: false },
|
||||||
{ id: "resource", label: "资源对接", icon: "👥", color: "#7B61FF", scenario: "resource" },
|
{ id: "investor", label: "资源对接", icon: "👥", color: "#7B61FF", joinable: true },
|
||||||
{ id: "mentor", label: "导师顾问", icon: "❤️", color: "#E91E63", scenario: "mentor" },
|
{ id: "mentor", label: "导师顾问", icon: "❤️", color: "#E91E63", joinable: true },
|
||||||
|
{ id: "team", label: "团队招募", icon: "🎮", color: "#4CAF50", joinable: true },
|
||||||
]
|
]
|
||||||
|
|
||||||
interface JoinModalProps {
|
// 获取本地存储的手机号
|
||||||
isOpen: boolean
|
const getStoredPhone = (): string => {
|
||||||
onClose: () => void
|
if (typeof window !== "undefined") {
|
||||||
type: (typeof matchTypes)[0]
|
return localStorage.getItem("user_phone") || ""
|
||||||
|
}
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
function JoinModal({ isOpen, onClose, type }: JoinModalProps) {
|
// 保存手机号到本地存储
|
||||||
const [phone, setPhone] = useState("")
|
const savePhone = (phone: string) => {
|
||||||
const [name, setName] = useState("")
|
if (typeof window !== "undefined") {
|
||||||
const [remark, setRemark] = useState("")
|
localStorage.setItem("user_phone", phone)
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
||||||
const [submitResult, setSubmitResult] = useState<{ success: boolean; message: string } | null>(null)
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!phone) {
|
|
||||||
setSubmitResult({ success: false, message: "请输入手机号" })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 简单的手机号验证
|
|
||||||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
|
||||||
setSubmitResult({ success: false, message: "请输入正确的手机号" })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSubmitting(true)
|
|
||||||
setSubmitResult(null)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch("/api/cunkebao", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
phone,
|
|
||||||
name,
|
|
||||||
scenario: type.scenario,
|
|
||||||
remark,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await response.json()
|
|
||||||
setSubmitResult(result)
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
// 成功后清空表单
|
|
||||||
setPhone("")
|
|
||||||
setName("")
|
|
||||||
setRemark("")
|
|
||||||
// 3秒后关闭弹窗
|
|
||||||
setTimeout(() => {
|
|
||||||
onClose()
|
|
||||||
setSubmitResult(null)
|
|
||||||
}, 3000)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setSubmitResult({ success: false, message: "网络错误,请稍后重试" })
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isOpen) return null
|
|
||||||
|
|
||||||
const getTitle = () => {
|
|
||||||
switch (type.id) {
|
|
||||||
case "team":
|
|
||||||
return "加入切片团队"
|
|
||||||
case "resource":
|
|
||||||
return "加入资源群"
|
|
||||||
case "mentor":
|
|
||||||
return "预约导师顾问"
|
|
||||||
default:
|
|
||||||
return "加入"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getDescription = () => {
|
|
||||||
switch (type.id) {
|
|
||||||
case "team":
|
|
||||||
return "加入切片团队,一起创造价值,共享收益"
|
|
||||||
case "resource":
|
|
||||||
return "加入资源对接群,链接优质商业资源"
|
|
||||||
case "mentor":
|
|
||||||
return "预约一对一导师咨询,获取专业指导"
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AnimatePresence>
|
|
||||||
<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 p-4"
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
|
||||||
<motion.div
|
|
||||||
initial={{ scale: 0.9, opacity: 0 }}
|
|
||||||
animate={{ scale: 1, opacity: 1 }}
|
|
||||||
exit={{ scale: 0.9, opacity: 0 }}
|
|
||||||
className="bg-[#1c1c1e] rounded-2xl w-full max-w-sm overflow-hidden"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
{/* 头部 */}
|
|
||||||
<div className="relative p-6 pb-4">
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="absolute right-4 top-4 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 className="flex items-center gap-3 mb-2">
|
|
||||||
<span className="text-3xl">{type.icon}</span>
|
|
||||||
<h2 className="text-xl font-bold text-white">{getTitle()}</h2>
|
|
||||||
</div>
|
|
||||||
<p className="text-white/50 text-sm">{getDescription()}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 表单 */}
|
|
||||||
<div className="px-6 pb-6 space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-white/60 text-sm mb-2">手机号 *</label>
|
|
||||||
<input
|
|
||||||
type="tel"
|
|
||||||
value={phone}
|
|
||||||
onChange={(e) => setPhone(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:border-[#00E5FF]/50 focus:outline-none transition-colors"
|
|
||||||
maxLength={11}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-white/60 text-sm mb-2">姓名(选填)</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(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:border-[#00E5FF]/50 focus:outline-none transition-colors"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-white/60 text-sm mb-2">备注(选填)</label>
|
|
||||||
<textarea
|
|
||||||
value={remark}
|
|
||||||
onChange={(e) => setRemark(e.target.value)}
|
|
||||||
placeholder="简单介绍一下您自己"
|
|
||||||
rows={3}
|
|
||||||
className="w-full px-4 py-3 rounded-xl bg-black/30 border border-white/10 text-white placeholder-white/30 focus:border-[#00E5FF]/50 focus:outline-none transition-colors resize-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 提交结果提示 */}
|
|
||||||
{submitResult && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: -10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className={`p-3 rounded-xl text-sm ${
|
|
||||||
submitResult.success ? "bg-green-500/20 text-green-400" : "bg-red-500/20 text-red-400"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{submitResult.message}
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 提交按钮 */}
|
|
||||||
<button
|
|
||||||
onClick={handleSubmit}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
className="w-full py-4 rounded-xl bg-[#00E5FF] text-black font-medium disabled:opacity-50 disabled:cursor-not-allowed transition-opacity"
|
|
||||||
>
|
|
||||||
{isSubmitting ? "提交中..." : "立即加入"}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<p className="text-white/30 text-xs text-center">提交后我们会尽快与您联系</p>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
</AnimatePresence>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MatchPage() {
|
export default function MatchPage() {
|
||||||
const [isMatching, setIsMatching] = useState(false)
|
const [isMatching, setIsMatching] = useState(false)
|
||||||
const [currentMatch, setCurrentMatch] = useState<MatchUser | null>(null)
|
const [currentMatch, setCurrentMatch] = useState<MatchUser | null>(null)
|
||||||
const [matchAttempts, setMatchAttempts] = useState(0)
|
const [matchAttempts, setMatchAttempts] = useState(0)
|
||||||
const [selectedType, setSelectedType] = useState(matchTypes[0])
|
const [selectedType, setSelectedType] = useState("partner")
|
||||||
const [showJoinModal, setShowJoinModal] = useState(false)
|
|
||||||
const [joinType, setJoinType] = useState(matchTypes[0])
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
const [showJoinModal, setShowJoinModal] = useState(false)
|
||||||
|
const [joinType, setJoinType] = useState<string | null>(null)
|
||||||
|
const [phoneNumber, setPhoneNumber] = useState("")
|
||||||
|
const [isJoining, setIsJoining] = useState(false)
|
||||||
|
const [joinSuccess, setJoinSuccess] = useState(false)
|
||||||
|
const [joinError, setJoinError] = useState("")
|
||||||
|
|
||||||
|
// 初始化时读取已存储的手机号
|
||||||
|
useEffect(() => {
|
||||||
|
const storedPhone = getStoredPhone()
|
||||||
|
if (storedPhone) {
|
||||||
|
setPhoneNumber(storedPhone)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleJoinSubmit = async () => {
|
||||||
|
if (!phoneNumber || phoneNumber.length !== 11) {
|
||||||
|
setJoinError("请输入正确的11位手机号")
|
||||||
|
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: phoneNumber,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// 保存手机号以便下次使用
|
||||||
|
savePhone(phoneNumber)
|
||||||
|
setJoinSuccess(true)
|
||||||
|
// 2秒后关闭弹窗
|
||||||
|
setTimeout(() => {
|
||||||
|
setShowJoinModal(false)
|
||||||
|
setJoinSuccess(false)
|
||||||
|
}, 2000)
|
||||||
|
} else {
|
||||||
|
setJoinError(result.message || "加入失败,请稍后重试")
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setJoinError("网络错误,请检查网络后重试")
|
||||||
|
} finally {
|
||||||
|
setIsJoining(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const startMatch = () => {
|
const startMatch = () => {
|
||||||
setIsMatching(true)
|
setIsMatching(true)
|
||||||
setMatchAttempts(0)
|
setMatchAttempts(0)
|
||||||
@@ -253,7 +154,7 @@ export default function MatchPage() {
|
|||||||
id: `user_${Date.now()}`,
|
id: `user_${Date.now()}`,
|
||||||
nickname: nicknames[randomIndex],
|
nickname: nicknames[randomIndex],
|
||||||
avatar: `https://picsum.photos/200/200?random=${randomIndex}`,
|
avatar: `https://picsum.photos/200/200?random=${randomIndex}`,
|
||||||
tags: ["创业者", "私域运营", selectedType.label],
|
tags: ["创业者", "私域运营", matchTypes.find((t) => t.id === selectedType)?.label || ""],
|
||||||
matchScore: Math.floor(Math.random() * 20) + 80,
|
matchScore: Math.floor(Math.random() * 20) + 80,
|
||||||
concept: concepts[randomIndex % concepts.length],
|
concept: concepts[randomIndex % concepts.length],
|
||||||
wechat: wechats[randomIndex % wechats.length],
|
wechat: wechats[randomIndex % wechats.length],
|
||||||
@@ -282,10 +183,8 @@ export default function MatchPage() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleTypeClick = (type: (typeof matchTypes)[0]) => {
|
const currentTypeLabel = matchTypes.find((t) => t.id === selectedType)?.label || "创业合伙"
|
||||||
setJoinType(type)
|
const joinTypeLabel = matchTypes.find((t) => t.id === joinType)?.label || ""
|
||||||
setShowJoinModal(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-black pb-24">
|
<div className="min-h-screen bg-black pb-24">
|
||||||
@@ -377,28 +276,48 @@ export default function MatchPage() {
|
|||||||
{/* 中心图标 */}
|
{/* 中心图标 */}
|
||||||
<Mic className="w-12 h-12 text-white/90 mb-3 relative z-10" />
|
<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-xl font-bold text-white mb-1 relative z-10">开始匹配</div>
|
||||||
<div className="text-sm text-white/60 relative z-10">寻找{selectedType.label}</div>
|
<div className="text-sm text-white/60 relative z-10">寻找{currentTypeLabel}</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* 当前模式显示 */}
|
{/* 当前模式显示 */}
|
||||||
<p className="text-white/50 text-sm mb-8">
|
<p className="text-white/50 text-sm mb-8">
|
||||||
当前模式: <span className="text-[#00E5FF]">{selectedType.label}</span>
|
当前模式: <span className="text-[#00E5FF]">{currentTypeLabel}</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* 分隔线 */}
|
{/* 分隔线 */}
|
||||||
<div className="w-full h-px bg-white/10 mb-6" />
|
<div className="w-full h-px bg-white/10 mb-6" />
|
||||||
|
|
||||||
|
{/* 选择匹配类型 - 修改为点击可加入的类型时弹出加入框 */}
|
||||||
<p className="text-white/40 text-sm mb-4">选择匹配类型</p>
|
<p className="text-white/40 text-sm mb-4">选择匹配类型</p>
|
||||||
<div className="grid grid-cols-3 gap-4 w-full">
|
<div className="grid grid-cols-4 gap-3 w-full">
|
||||||
{matchTypes.map((type) => (
|
{matchTypes.map((type) => (
|
||||||
<button
|
<button
|
||||||
key={type.id}
|
key={type.id}
|
||||||
onClick={() => handleTypeClick(type)}
|
onClick={() => {
|
||||||
className="p-5 rounded-xl flex flex-col items-center gap-3 transition-all bg-[#1c1c1e] border border-white/10 hover:border-[#00E5FF]/50 hover:bg-[#00E5FF]/5 active:scale-95"
|
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-3xl">{type.icon}</span>
|
<span className="text-2xl">{type.icon}</span>
|
||||||
<span className="text-sm text-white/80">{type.label}</span>
|
<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>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -537,7 +456,89 @@ export default function MatchPage() {
|
|||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
<JoinModal isOpen={showJoinModal} onClose={() => setShowJoinModal(false)} type={joinType} />
|
<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">
|
||||||
|
{getStoredPhone() ? "检测到您已注册的手机号,确认后即可加入" : "请输入您的手机号以便我们联系您"}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* 手机号输入 */}
|
||||||
|
<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}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 错误提示 */}
|
||||||
|
{joinError && <p className="text-red-400 text-sm mb-4">{joinError}</p>}
|
||||||
|
|
||||||
|
{/* 提交按钮 */}
|
||||||
|
<button
|
||||||
|
onClick={handleJoinSubmit}
|
||||||
|
disabled={isJoining || !phoneNumber}
|
||||||
|
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">
|
<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="px-4 py-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user