仪表盘
一个网店的管理后台。切换时间段后,数据卡片和图表会变成该时间段的数据。
仪表盘
要点
- 1
数字从原始数据计算得出
销售额、订单数等合计,用 reduce 把每天的数据加起来得到。切换时间段后,只要重新计算就能对得上。
- 2
图表交给库来画
柱子、坐标轴刻度以及鼠标悬停时显示的数字,都由 Recharts 绘制。你只需要告诉它“用哪份数据、画哪个字段”。
- 3
用颜色和符号表示涨跌
和上一个时间段相比,增加显示绿色的 ▲,减少显示红色的 ▼。不用看数字,一眼就能看出好坏。
使用的工具
点击查看第 2 部分的比较
修改代码
修改代码,右边的预览会跟着变化。
import { useState } from "react" import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts" import { Button } from "./components/ui/button" // 每天的销售额和订单数(真实项目中从服务器获取)。这里把今天当作 10/4 const weekDays = [ { day: "9/28", sales: 7070, orders: 38 }, { day: "9/29", sales: 5950, orders: 33 }, { day: "9/30", sales: 6580, orders: 36 }, { day: "10/1", sales: 8190, orders: 44 }, { day: "10/2", sales: 4550, orders: 27 }, { day: "10/3", sales: 10010, orders: 61 }, { day: "10/4", sales: 10990, orders: 64 }, ] // 近30天(9/5~10/4)= 9/5~9/27 的23天 + 上面的近7天(同一天的数字保持一致) // 这23天用固定公式生成(仅供演示,不用记) const monthDays = [ ...Array.from({ length: 23 }, (_, i) => ({ day: `9/${i + 5}`, sales: 5110 + ((i * 37) % 11) * 560 + (i % 7 <= 1 ? 3290 : 0), orders: 30 + ((i * 37) % 11) * 3 + (i % 7 <= 1 ? 18 : 0), })), ...weekDays, ] // 各时间段的数据。previous 是“上一个时间段”的数字(用来比较涨跌) const periods = { week: { label: "近7天", days: weekDays, members: 48, previous: { sales: 47600, orders: 286, average: 168, members: 40 } }, month: { label: "近30天", days: monthDays, members: 186, previous: { sales: 231000, orders: 1390, average: 168, members: 201 } }, } // 最近订单 const recentOrders = [ { id: "A-1052", name: "张伟", amount: 595, status: "已发货" }, { id: "A-1051", name: "李娜", amount: 210, status: "备货中" }, { id: "A-1050", name: "王芳", amount: 1085, status: "已发货" }, { id: "A-1049", name: "刘洋", amount: 140, status: "备货中" }, { id: "A-1048", name: "陈静", amount: 350, status: "已发货" }, ] // 金额格式(7070 → ¥7,070) function yen(value) { return "¥" + value.toLocaleString() } // 与上一时间段相比增长了百分之几(下降时为负数)。例:本期 120、上期 100 → 20 function changePercent(value, previous) { return Math.round(((value - previous) / previous) * 100) } export default function App() { // ① 当前选中的时间段("week" = 近7天,"month" = 近30天),保存在 state 中 //const period = "week" const [period, setPeriod] = useState("week") const current = periods[period] // 所选时间段的数据 // ② 时间段内的合计 //const sales = 1144000 // 每天销售额(d.sales)之和 const sales = current.days.reduce((sum, d) => sum + d.sales, 0) //const orders = 303 // 每天订单数(d.orders)之和 const orders = current.days.reduce((sum, d) => sum + d.orders, 0) const average = Math.round(sales / orders) // 客单价 = 销售额 ÷ 订单数 // 4张数据卡片的数据(label = 名称,text = 大号显示的数字,change = 增长百分比) const stats = [ { label: "销售额", text: yen(sales), change: changePercent(sales, current.previous.sales) }, { label: "订单数", text: orders.toLocaleString() + "单", change: changePercent(orders, current.previous.orders) }, { label: "客单价", text: yen(average), change: changePercent(average, current.previous.average) }, { label: "新增会员", text: current.members + "人", change: changePercent(current.members, current.previous.members) }, ] return ( <div className="mx-auto my-6 flex w-full max-w-4xl flex-col gap-6 px-4 text-foreground"> {/* 标题和时间段按钮 */} <div className="flex flex-wrap items-center justify-between gap-3"> <h1 className="text-xl font-semibold">数据看板</h1> <div className="flex gap-2"> {/* ③ 点击切换时间段。选中的为实心(default),另一个为描边(outline) */} <Button size="sm" variant={period === "week" ? "default" : "outline"} onClick={() => setPeriod("week")}> 近7天 </Button> <Button size="sm" variant={period === "month" ? "default" : "outline"} onClick={() => setPeriod("month")}> 近30天 </Button> </div> </div> {/* ④ 数据卡片:用 map 列出 4 项 stats */} <div className="grid grid-cols-2 gap-3 md:grid-cols-4"> {stats.map((stat) => ( <div key={stat.label} className="rounded-xl border bg-card p-4 text-card-foreground"> <p className="text-xs text-muted-foreground">{stat.label}</p> <p className="mt-1 text-xl font-semibold tabular-nums">{stat.text}</p> {/* ⑤ 上升(change 大于等于 0)显示绿色 ▲,下降显示红色 ▼ */} {/* <p className="mt-1 text-xs text-emerald-600"> */} <p className={stat.change >= 0 ? "mt-1 text-xs text-emerald-600" : "mt-1 text-xs text-red-600"}> {stat.change >= 0 ? "▲" : "▼"} {Math.abs(stat.change)}% <span className="text-muted-foreground"> 较上期</span> </p> </div> ))} </div> {/* 销售额图表(Recharts 把传给 data 的列表画成柱状图) */} <div className="rounded-xl border bg-card p-4 text-card-foreground"> <p className="text-sm font-medium">销售额趋势</p> <div className="mt-4 h-56"> <ResponsiveContainer width="100%" height="100%"> <BarChart data={current.days}> <CartesianGrid vertical={false} stroke="var(--border)" /> <XAxis dataKey="day" tickLine={false} axisLine={false} fontSize={12} /> <YAxis tickLine={false} axisLine={false} fontSize={12} width={40} tickFormatter={(v) => "¥" + v / 1000 + "k"} /> <Tooltip formatter={(v) => [yen(v), "销售额"]} cursor={{ fill: "var(--muted)" }} /> <Bar dataKey="sales" fill="var(--primary)" radius={[4, 4, 0, 0]} /> </BarChart> </ResponsiveContainer> </div> </div> {/* 最近订单 */} <div className="rounded-xl border bg-card p-4 text-card-foreground"> <p className="text-sm font-medium">最近订单</p> <ul className="mt-3 divide-y text-sm"> {recentOrders.map((order) => ( <li key={order.id} className="flex items-center gap-3 py-2.5"> <span className="w-16 text-muted-foreground tabular-nums">{order.id}</span> <span className="flex-1">{order.name}</span> <span className="tabular-nums">{yen(order.amount)}</span> <span // ⑥ “已发货”显示绿色,其他(备货中)显示黄色 className={order.status === "已发货" ? "w-16 rounded-full bg-emerald-100 py-0.5 text-center text-xs text-emerald-700" : "w-16 rounded-full bg-amber-100 py-0.5 text-center text-xs text-amber-700"} > {order.status} </span> </li> ))} </ul> </div> </div > ) }