Files
soul/components/matching-circle.tsx
v0 59ca3b2bbd refactor: full product interaction system redesign
Refactor homepage, reading modal, matching feature, and user profile for improved UX

#VERCEL_SKIP

Co-authored-by: undefined <undefined+undefined@users.noreply.github.com>
2026-01-14 05:17:59 +00:00

172 lines
6.4 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 { Sparkles, Users, BookOpen } from "lucide-react"
import { getAllSections } from "@/lib/book-data"
import { useStore } from "@/lib/store"
export function MatchingCircle() {
const [isMatching, setIsMatching] = useState(false)
const [matchProgress, setMatchProgress] = useState(0)
const [matchResult, setMatchResult] = useState<{
section: { id: string; title: string }
reason: string
compatibility: number
} | null>(null)
const { user, isLoggedIn } = useStore()
const allSections = getAllSections()
// 开始匹配
const startMatching = () => {
if (isMatching) return
setIsMatching(true)
setMatchProgress(0)
setMatchResult(null)
// 模拟匹配进度
const interval = setInterval(() => {
setMatchProgress((prev) => {
if (prev >= 100) {
clearInterval(interval)
// 匹配完成,生成结果
const randomSection = allSections[Math.floor(Math.random() * allSections.length)]
const reasons = [
"与你的创业方向高度匹配",
"适合你当前的发展阶段",
"契合你的商业思维模式",
"与你的行业背景相关",
"符合你的学习偏好",
]
setMatchResult({
section: { id: randomSection.id, title: randomSection.title },
reason: reasons[Math.floor(Math.random() * reasons.length)],
compatibility: Math.floor(Math.random() * 20) + 80,
})
setIsMatching(false)
return 100
}
return prev + 2
})
}, 50)
}
// 保存匹配结果到本地
useEffect(() => {
if (matchResult && isLoggedIn) {
const savedResults = JSON.parse(localStorage.getItem("match_results") || "[]")
savedResults.unshift({
...matchResult,
userId: user?.id,
matchedAt: new Date().toISOString(),
})
// 只保留最近10条
localStorage.setItem("match_results", JSON.stringify(savedResults.slice(0, 10)))
}
}, [matchResult, isLoggedIn, user?.id])
return (
<div className="w-full max-w-sm text-center">
{/* 匹配圆环 */}
<div className="relative w-64 h-64 mx-auto mb-8">
{/* 外圈装饰 */}
<div className="absolute inset-0 rounded-full border-2 border-[var(--app-border)] opacity-30" />
<div className="absolute inset-2 rounded-full border border-[var(--app-border)] opacity-20" />
<div className="absolute inset-4 rounded-full border border-[var(--app-border)] opacity-10" />
{/* 进度圆环 */}
<svg className="absolute inset-0 w-full h-full -rotate-90">
<circle cx="128" cy="128" r="120" fill="none" stroke="var(--app-bg-tertiary)" strokeWidth="4" />
<circle
cx="128"
cy="128"
r="120"
fill="none"
stroke="var(--app-brand)"
strokeWidth="4"
strokeLinecap="round"
strokeDasharray={`${2 * Math.PI * 120}`}
strokeDashoffset={`${2 * Math.PI * 120 * (1 - matchProgress / 100)}`}
className="transition-all duration-100"
style={{
filter: isMatching ? "drop-shadow(0 0 10px var(--app-brand))" : "none",
}}
/>
</svg>
{/* 中心内容 */}
<div className="absolute inset-8 rounded-full glass-card flex flex-col items-center justify-center">
{isMatching ? (
<>
<Sparkles className="w-10 h-10 text-[var(--app-brand)] animate-pulse mb-2" />
<p className="text-[var(--app-brand)] text-2xl font-bold">{matchProgress}%</p>
<p className="text-[var(--app-text-tertiary)] text-xs">...</p>
</>
) : matchResult ? (
<>
<div className="text-[var(--app-brand)] text-3xl font-bold mb-1">{matchResult.compatibility}%</div>
<p className="text-white text-xs mb-1"></p>
<p className="text-[var(--app-text-tertiary)] text-[10px]">{matchResult.reason}</p>
</>
) : (
<>
<Users className="w-10 h-10 text-[var(--app-text-tertiary)] mb-2" />
<p className="text-white text-sm"></p>
<p className="text-[var(--app-text-tertiary)] text-xs"></p>
</>
)}
</div>
{/* 浮动装饰点 */}
{isMatching && (
<>
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-2 h-2 rounded-full bg-[var(--app-brand)] animate-ping" />
<div
className="absolute bottom-0 left-1/2 -translate-x-1/2 w-2 h-2 rounded-full bg-[var(--ios-blue)] animate-ping"
style={{ animationDelay: "0.5s" }}
/>
<div
className="absolute left-0 top-1/2 -translate-y-1/2 w-2 h-2 rounded-full bg-[var(--ios-purple)] animate-ping"
style={{ animationDelay: "0.25s" }}
/>
<div
className="absolute right-0 top-1/2 -translate-y-1/2 w-2 h-2 rounded-full bg-[var(--ios-teal)] animate-ping"
style={{ animationDelay: "0.75s" }}
/>
</>
)}
</div>
{/* 匹配结果 */}
{matchResult && (
<div className="glass-card p-4 mb-6 text-left">
<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">
<BookOpen className="w-5 h-5 text-[var(--app-brand)]" />
</div>
<div className="flex-1 min-w-0">
<p className="text-[var(--app-brand)] text-xs mb-0.5">{matchResult.section.id}</p>
<p className="text-white text-sm truncate">{matchResult.section.title}</p>
</div>
</div>
</div>
)}
{/* 匹配按钮 */}
<button
onClick={startMatching}
disabled={isMatching}
className={`btn-ios w-full flex items-center justify-center gap-2 ${
isMatching ? "opacity-50 cursor-not-allowed" : "glow"
}`}
>
<Sparkles className="w-5 h-5" />
<span>{isMatching ? "匹配中..." : matchResult ? "重新匹配" : "开始匹配"}</span>
</button>
<p className="text-[var(--app-text-tertiary)] text-xs mt-4"></p>
</div>
)
}