127 lines
3.2 KiB
TypeScript
127 lines
3.2 KiB
TypeScript
"use client";
|
||
|
||
import * as React from "react";
|
||
import { cn } from "@/lib/utils";
|
||
|
||
export type TrendDirection = "up" | "down" | "neutral";
|
||
|
||
export interface SparklinePoint {
|
||
value: number;
|
||
}
|
||
|
||
export interface MetricCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
||
/** 小号大写字母标签(如 "TOTAL JOBS TODAY") */
|
||
label: string;
|
||
/** 主数字/值 */
|
||
value: string | number;
|
||
/** 次要描述,显示在主值下方 */
|
||
subValue?: string;
|
||
/** 趋势方向 */
|
||
trend?: TrendDirection;
|
||
/** 趋势百分比,如 "+18%" */
|
||
trendValue?: string;
|
||
/** 趋势说明文字,如 "vs last month" */
|
||
trendLabel?: string;
|
||
/** sparkline 数据点 */
|
||
sparklineData?: SparklinePoint[];
|
||
/** 图标(可选的左侧小图标) */
|
||
icon?: React.ReactNode;
|
||
/** 卡片尺寸 */
|
||
size?: "sm" | "default" | "lg";
|
||
}
|
||
|
||
const MetricCard = React.forwardRef<HTMLDivElement, MetricCardProps>(
|
||
(
|
||
{
|
||
className,
|
||
label,
|
||
value,
|
||
subValue,
|
||
trend = "neutral",
|
||
trendValue,
|
||
trendLabel,
|
||
sparklineData,
|
||
icon,
|
||
size = "default",
|
||
...props
|
||
},
|
||
ref
|
||
) => {
|
||
const paddingClass = {
|
||
sm: "p-4",
|
||
default: "p-5",
|
||
lg: "p-6",
|
||
}[size];
|
||
|
||
const valueClass = {
|
||
sm: "text-2xl",
|
||
default: "text-3xl",
|
||
lg: "text-4xl",
|
||
}[size];
|
||
|
||
return (
|
||
<div
|
||
ref={ref}
|
||
className={cn(
|
||
"bg-white rounded-xl border border-gray-200",
|
||
className
|
||
)}
|
||
{...props}
|
||
>
|
||
<div className={cn("flex items-stretch", paddingClass)}>
|
||
{/* Left color bar */}
|
||
<div
|
||
className={cn(
|
||
"w-1 shrink-0 rounded-full mr-4",
|
||
trend === "up" && "bg-emerald-500",
|
||
trend === "down" && "bg-red-500",
|
||
trend === "neutral" && "bg-gray-300"
|
||
)}
|
||
/>
|
||
|
||
{/* Content */}
|
||
<div className="flex-1 min-w-0">
|
||
{/* Label row */}
|
||
<div className="flex items-center justify-between">
|
||
<p className="text-sm font-medium text-gray-500">
|
||
{label}
|
||
</p>
|
||
{trendValue && (
|
||
<span
|
||
className={cn(
|
||
"text-xs font-medium px-2 py-0.5 rounded-full",
|
||
trend === "up" && "text-emerald-600 bg-emerald-50",
|
||
trend === "down" && "text-red-600 bg-red-50",
|
||
trend === "neutral" && "text-gray-500 bg-gray-50"
|
||
)}
|
||
>
|
||
{trend === "up" && "+"}{trendValue}
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* Value */}
|
||
<p
|
||
className={cn(
|
||
"mt-2 font-bold text-gray-900 leading-none",
|
||
valueClass
|
||
)}
|
||
style={{ fontVariantNumeric: "tabular-nums" }}
|
||
>
|
||
{value}
|
||
</p>
|
||
|
||
{/* Trend label */}
|
||
{trendLabel && (
|
||
<p className="mt-1 text-xs text-gray-400">{trendLabel}</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
);
|
||
MetricCard.displayName = "MetricCard";
|
||
|
||
export { MetricCard };
|