Remotion LabRemotion Lab
返回模板庫

統計數字卡片

三欄式商務統計數字卡片,每張卡片依序彈入,大數字從零計數至目標值,搭配趨勢標籤與彩色頂部邊框,適合業績報告與數據摘要場景。

圖表商務簡約
提示詞(可直接修改內容)
import {
  AbsoluteFill,
  interpolate,
  spring,
  useCurrentFrame,
  useVideoConfig,
} from "remotion";
import React from "react";

const CARDS = [
  { label: "總用戶數", value: 128450, unit: "人", trend: "+12.5%", color: "#3b82f6" },
  { label: "月收益", value: 892000, unit: "元", trend: "+8.3%", color: "#10b981" },
  { label: "轉換率", value: 3.7, unit: "%", trend: "+0.4%", color: "#8b5cf6", isDecimal: true },
];

interface CardProps {
  label: string;
  value: number;
  unit: string;
  trend: string;
  color: string;
  isDecimal?: boolean;
  frame: number;
  fps: number;
  startFrame: number;
}

const StatCard: React.FC<CardProps> = ({
  label,
  value,
  unit,
  trend,
  color,
  isDecimal,
  frame,
  fps,
  startFrame,
}) => {
  const slideProgress = spring({
    frame: frame - startFrame,
    fps,
    config: { damping: 22, stiffness: 120 },
  });

  const y = interpolate(slideProgress, [0, 1], [80, 0]);
  const opacity = interpolate(slideProgress, [0, 0.4], [0, 1], {
    extrapolateRight: "clamp",
  });

  const countProgress = interpolate(frame, [30, 90], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  const currentValue = isDecimal
    ? parseFloat((value * countProgress).toFixed(1))
    : Math.floor(value * countProgress);

  const displayValue = isDecimal
    ? currentValue.toFixed(1)
    : currentValue.toLocaleString("zh-TW");

  return (
    <div
      style={{
        transform: `translateY(${y}px)`,
        opacity,
        width: 480,
        background: "#1a1a1a",
        borderRadius: 16,
        borderTop: `4px solid ${color}`,
        padding: "48px 52px 44px",
        boxSizing: "border-box",
        display: "flex",
        flexDirection: "column",
        gap: 12,
      }}
    >
      <div
        style={{
          fontSize: 22,
          color: "#9ca3af",
          fontFamily: "sans-serif",
          fontWeight: 500,
          letterSpacing: 2,
        }}
      >
        {label}
      </div>
      <div
        style={{
          display: "flex",
          alignItems: "flex-end",
          gap: 10,
        }}
      >
        <span
          style={{
            fontSize: 80,
            fontWeight: 800,
            color: "#ffffff",
            fontFamily: "sans-serif",
            lineHeight: 1,
          }}
        >
          {displayValue}
        </span>
        <span
          style={{
            fontSize: 28,
            fontWeight: 600,
            color: "#d1d5db",
            fontFamily: "sans-serif",
            paddingBottom: 10,
          }}
        >
          {unit}
        </span>
      </div>
      <div
        style={{
          display: "flex",
          alignItems: "center",
          gap: 8,
          marginTop: 4,
        }}
      >
        <span
          style={{
            fontSize: 20,
            fontWeight: 700,
            color,
            fontFamily: "sans-serif",
            background: `${color}22`,
            padding: "4px 14px",
            borderRadius: 20,
          }}
        >
          {trend}
        </span>
        <span
          style={{
            fontSize: 18,
            color: "#6b7280",
            fontFamily: "sans-serif",
          }}
        >
          較上月
        </span>
      </div>
    </div>
  );
};

export const CounterCard: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  return (
    <AbsoluteFill
      style={{
        background: "#0f0f0f",
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        justifyContent: "center",
        gap: 0,
      }}
    >
      {/* 標題 */}
      <div
        style={{
          fontSize: 32,
          fontWeight: 700,
          color: "#4b5563",
          fontFamily: "sans-serif",
          letterSpacing: 4,
          marginBottom: 56,
          textTransform: "uppercase",
        }}
      >
        核心業績概覽
      </div>
      {/* 卡片列 */}
      <div
        style={{
          display: "flex",
          flexDirection: "row",
          gap: 40,
          alignItems: "stretch",
        }}
      >
        {CARDS.map((card, index) => (
          <StatCard
            key={card.label}
            {...card}
            frame={frame}
            fps={fps}
            startFrame={index * 20}
          />
        ))}
      </div>
    </AbsoluteFill>
  );
};

登入後查看完整程式碼