248 lines
6.2 KiB
TypeScript
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>
|
|
);
|
|
}
|