geo/frontend/components/dashboard/HealthOverviewCards.tsx

248 lines
6.2 KiB
TypeScript

"use client";
import { Card, CardContent } from "@/components/ui/card";
import { cn } from "@/lib/utils";
import {
TrendingUp,
TrendingDown,
Minus,
AlertCircle,
CheckCircle,
XCircle,
HelpCircle,
} from "lucide-react";
import { HealthLevel, HEALTH_LEVEL_CONFIG } from "@/types/dashboard-health";
import { getTrendStyle, getCompetitorStatus } from "@/lib/dashboard-health";
interface OverviewCardProps {
className?: string;
children: React.ReactNode;
}
function OverviewCard({ className, children }: OverviewCardProps) {
return (
<Card className={cn("flex-1 min-w-[200px]", className)}>
<CardContent className="p-6">{children}</CardContent>
</Card>
);
}
// 综合评分卡片
interface HealthScoreCardProps {
score: number;
healthLevel?: HealthLevel;
}
export function HealthScoreCard({ score, healthLevel }: HealthScoreCardProps) {
const level =
healthLevel ??
(score >= 80
? "excellent"
: score >= 60
? "good"
: score >= 40
? "pass"
: "danger");
const config = HEALTH_LEVEL_CONFIG[level];
const LevelIcon = {
excellent: CheckCircle,
good: HelpCircle,
pass: AlertCircle,
danger: XCircle,
}[level];
return (
<OverviewCard>
<div className="space-y-2">
{/* 标签 */}
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground"></span>
<span className="text-muted-foreground">/100</span>
</div>
{/* 大数字 */}
<div className="flex items-end gap-2">
<span className="text-5xl font-bold">{score.toFixed(0)}</span>
</div>
{/* 健康等级标签 */}
<div
className={cn(
"inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-sm font-medium",
config.color.bg,
config.color.text,
)}
>
<LevelIcon className="h-4 w-4" />
<span>{config.label}</span>
</div>
</div>
</OverviewCard>
);
}
// 竞品地位卡片
interface CompetitorStatusCardProps {
ahead: number;
behind: number;
}
export function CompetitorStatusCard({
ahead,
behind,
}: CompetitorStatusCardProps) {
const status = getCompetitorStatus(ahead, behind);
const statusIcon =
status.status === "leading" ? (
<TrendingUp className="h-4 w-4 text-emerald-600" />
) : status.status === "trailing" ? (
<TrendingDown className="h-4 w-4 text-red-600" />
) : (
<Minus className="h-4 w-4 text-yellow-600" />
);
return (
<OverviewCard>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground"></span>
{statusIcon}
</div>
<div className={cn("text-3xl font-bold", status.color)}>
{ahead > 0
? `领先${ahead}竞品`
: behind > 0
? `落后${behind}竞品`
: status.label}
</div>
{ahead > 0 && behind > 0 && (
<div className="flex gap-3 text-sm">
<span className="text-emerald-600">{ahead}</span>
<span className="text-red-600">{behind}</span>
</div>
)}
</div>
</OverviewCard>
);
}
// 趋势卡片
interface TrendCardProps {
change: number;
label?: string;
}
export function TrendCard({ change, label = "较上周" }: TrendCardProps) {
const trend = getTrendStyle(change);
const TrendIcon =
trend.icon === "up"
? TrendingUp
: trend.icon === "down"
? TrendingDown
: Minus;
return (
<OverviewCard>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground"></span>
<TrendIcon className={cn("h-4 w-4", trend.color)} />
</div>
<div className="flex items-center gap-2">
<span className={cn("text-3xl font-bold", trend.color)}>
{trend.text}
</span>
</div>
<span className="text-sm text-muted-foreground">{label}</span>
</div>
</OverviewCard>
);
}
// 监控平台卡片
interface MonitorPlatformCardProps {
monitored: number;
total: number;
}
export function MonitorPlatformCard({
monitored,
total,
}: MonitorPlatformCardProps) {
const progress = total > 0 ? (monitored / total) * 100 : 0;
const isFull = monitored === total;
return (
<OverviewCard>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground"></span>
<span
className={cn(
"text-sm font-medium",
isFull ? "text-emerald-600" : "text-blue-600",
)}
>
{isFull ? "全部监控" : "部分监控"}
</span>
</div>
<div className="flex items-end gap-2">
<span className="text-3xl font-bold">{monitored}</span>
<span className="text-lg text-muted-foreground">/{total}</span>
</div>
{/* 进度条 */}
<div className="h-2 w-full overflow-hidden rounded-full bg-gray-100">
<div
className={cn(
"h-full rounded-full transition-all",
isFull ? "bg-emerald-500" : "bg-blue-500",
)}
style={{ width: `${progress}%` }}
/>
</div>
<span className="text-sm text-muted-foreground"></span>
</div>
</OverviewCard>
);
}
// 概览卡片组
interface OverviewCardsProps {
score: number;
healthLevel?: HealthLevel;
ahead: number;
behind: number;
change: number;
monitored: number;
total: number;
}
export function OverviewCards({
score,
healthLevel,
ahead,
behind,
change,
monitored,
total,
}: OverviewCardsProps) {
return (
<div className="grid gap-4 grid-cols-2 lg:grid-cols-4">
<HealthScoreCard score={score} healthLevel={healthLevel} />
<CompetitorStatusCard ahead={ahead} behind={behind} />
<TrendCard change={change} />
<MonitorPlatformCard monitored={monitored} total={total} />
</div>
);
}