geo/frontend/components/business/metric-card.tsx

127 lines
3.2 KiB
TypeScript
Raw Permalink 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 * 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 };