Files
wzdj/app/admin/screen/_components/stats-panel.tsx

205 lines
9.2 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 { Card, CardContent } from "@/components/ui/card"
import AnimatedNumber from "./animated-number"
import { Users, Gamepad2, CreditCard, ExternalLink, Star, Trophy, Zap, TrendingUp } from "lucide-react"
import { useEffect, useState, useCallback, useRef } from "react"
import { useDashboard } from "../_lib/contexts/DashboardContext"
export default function StatsPanel() {
const { config } = useDashboard()
const yesterdayRevenue = 117200
const targetRevenue = useRef(Math.floor(yesterdayRevenue * (0.95 + Math.random() * 0.1)))
const [data, setData] = useState({
todayRevenue: 0,
yesterdayRevenue: yesterdayRevenue,
totalFans: config.totalFans || 460000,
activeFans: Math.floor(((config.totalFans || 460000) * (config.activeUserRate ?? 8.5)) / 100),
championCount: config.championCount || 14,
totalStars: config.totalStars || 201,
superIndividualCount: config.superIndividualCount || 1407,
})
const [isAnimating, setIsAnimating] = useState(false)
const stepCountRef = useRef(0)
const calculateRevenue = useCallback(() => {
const now = new Date()
const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const endOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59)
// 计算当天已过去的时间比例
const dayProgress = (now.getTime() - startOfDay.getTime()) / (endOfDay.getTime() - startOfDay.getTime())
// 每5秒增长一次一天共 17280 次更新机会 (24*60*60/5)
// 使用缓动函数让增长更自然(开始慢,中间快,结束慢)
const easeInOut = (t: number) => (t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2)
const easedProgress = easeInOut(dayProgress)
// 当前收益 = 目标收益 * 缓动后的进度
const currentRevenue = Math.floor(targetRevenue.current * easedProgress)
// 添加小幅波动 ±0.5%
const fluctuation = 0.995 + Math.random() * 0.01
return Math.floor(currentRevenue * fluctuation)
}, [])
useEffect(() => {
// 初始计算
setData((prev) => ({ ...prev, todayRevenue: calculateRevenue() }))
const interval = setInterval(() => {
setIsAnimating(true)
stepCountRef.current += 1
setData((prev) => ({ ...prev, todayRevenue: calculateRevenue() }))
// 动画结束后重置状态
setTimeout(() => {
setIsAnimating(false)
}, 300)
}, 5000)
return () => clearInterval(interval)
}, [calculateRevenue])
const handleLinkClick = (url: string) => {
window.open(url, "_blank", "noopener,noreferrer")
}
const revenueCompare = ((data.todayRevenue / data.yesterdayRevenue) * 100 - 100).toFixed(1)
const isPositive = Number.parseFloat(revenueCompare) >= 0
return (
<div className="space-y-4">
{/* 收益统计卡片 */}
<Card className="wz-glass-card border-border/50 hover:border-white/20 transition-all duration-300 shadow-lg">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div className="flex items-center">
<div className="w-12 h-12 bg-white/10 rounded-xl flex items-center justify-center mr-3">
<CreditCard className="w-6 h-6 text-amber-400/90" />
</div>
<div>
<div className="text-sm text-muted-foreground flex items-center gap-2">
<span className="text-[10px] px-1.5 py-0.5 bg-muted rounded text-muted-foreground/70">5</span>
</div>
<div
className={`text-2xl font-bold text-foreground flex items-center transition-transform duration-300 ${isAnimating ? "revenue-pulse" : ""}`}
>
¥{data.todayRevenue.toLocaleString()}
<span
className={`ml-2 text-xs px-2 py-1 rounded-full ${isPositive ? "bg-green-500/20 text-green-400" : "bg-red-500/20 text-red-400"}`}
>
{isPositive ? "+" : ""}
{revenueCompare}%
</span>
</div>
</div>
</div>
</div>
<div className="mt-4 flex items-center justify-between pt-3 border-t border-border/50">
<div className="flex items-center">
<CreditCard className="w-5 h-5 mr-2 text-white/70" />
<div className="text-sm text-muted-foreground"></div>
<div className="text-lg font-semibold text-foreground ml-2">
¥{data.yesterdayRevenue.toLocaleString()}
</div>
</div>
<div className="text-xs px-2 py-1 bg-green-500/20 text-green-400 rounded-full flex items-center">
<TrendingUp className="w-3 h-3 mr-1" />+{config.yearOverYear?.toFixed(1) || "15.2"}%
</div>
</div>
</CardContent>
</Card>
{/* 粉丝数据卡片 */}
<Card className="wz-glass-card border-border/50 hover:border-white/20 transition-all duration-300 shadow-lg">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div className="flex items-center">
<div className="w-12 h-12 bg-white/10 rounded-xl flex items-center justify-center mr-3">
<Users className="w-6 h-6 text-white/80" />
</div>
<div>
<div className="text-sm text-muted-foreground"></div>
<div
className="text-2xl font-bold text-foreground flex items-center cursor-pointer hover:text-blue-400 transition-colors"
onClick={() => handleLinkClick("https://is-op.vercel.app/devices")}
>
<AnimatedNumber value={data.totalFans} formatValue={(val) => (val / 10000).toFixed(1) + "万"} />
<ExternalLink className="w-4 h-4 ml-2 text-muted-foreground" />
</div>
</div>
</div>
</div>
<div className="mt-4 flex items-center justify-between pt-3 border-t border-border/50">
<div className="flex items-center">
<Zap className="w-5 h-5 mr-2 text-green-400" />
<div>
<div className="text-sm text-muted-foreground"></div>
<div className="text-lg font-semibold text-foreground">
<AnimatedNumber value={data.activeFans} formatValue={(val) => (val / 10000).toFixed(1) + "万"} />
</div>
</div>
</div>
<div className="text-xs px-2 py-1 bg-green-500/20 text-green-400 rounded-full">
{config.activeUserRate || 8.5}%
</div>
</div>
</CardContent>
</Card>
{/* 电竞明星数据卡片 */}
<Card className="wz-glass-card border-border/50 hover:border-white/20 transition-all duration-300 shadow-lg">
<CardContent className="p-4">
<div className="space-y-4">
<div className="flex items-center justify-between p-2 bg-yellow-500/10 rounded-lg">
<div className="flex items-center">
<Trophy className="w-5 h-5 mr-2 text-yellow-500" />
<div className="text-sm text-muted-foreground"></div>
</div>
<div className="text-lg font-bold text-yellow-400">{data.championCount}</div>
</div>
<div className="flex items-center justify-between p-2 bg-blue-500/10 rounded-lg">
<div className="flex items-center">
<Star className="w-5 h-5 mr-2 text-blue-500" />
<div className="text-sm text-muted-foreground"></div>
</div>
<div className="text-lg font-bold text-blue-400">{data.totalStars}</div>
</div>
<div className="flex items-center justify-between p-2 bg-emerald-500/10 rounded-lg">
<div className="flex items-center">
<Gamepad2 className="w-5 h-5 mr-2 text-emerald-500" />
<div className="text-sm text-muted-foreground"></div>
</div>
<div className="text-lg font-bold text-emerald-400">{data.superIndividualCount}</div>
</div>
</div>
</CardContent>
</Card>
{/* 运营团队数据卡片 */}
<Card className="wz-glass-card border-border/50 hover:border-white/20 transition-all duration-300 shadow-lg">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div className="flex items-center">
<div className="w-12 h-12 bg-white/10 rounded-xl flex items-center justify-center mr-3">
<Users className="w-6 h-6 text-white/80" />
</div>
<div>
<div className="text-sm text-muted-foreground"></div>
<div className="text-2xl font-bold text-foreground">32</div>
</div>
</div>
<div className="text-xs px-3 py-1 bg-white/15 text-white/80 rounded-full"></div>
</div>
</CardContent>
</Card>
</div>
)
}