| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261 |
- "use client"
- import {Activity, Cpu, Droplets, Flame, Waves} from 'lucide-react'
- import MetricCard from "@/components/shared/metric-card"
- import Section from "@/components/shared/section"
- import {Button} from "@/components/ui/button"
- import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card"
- import {
- type ChartConfig,
- ChartContainer,
- ChartLegend,
- ChartLegendContent,
- ChartTooltip,
- ChartTooltipContent,
- } from "@/components/ui/chart"
- import {
- Area,
- Bar,
- BarChart,
- CartesianGrid,
- Legend,
- Line,
- LineChart,
- Pie,
- PieChart,
- RadialBar,
- RadialBarChart,
- ResponsiveContainer,
- XAxis,
- YAxis,
- } from "recharts"
- import {useMemo, useState} from "react"
- const industries = [
- { key: "gas", label: "燃气", icon: <Flame className="h-4 w-4 text-amber-600" /> },
- { key: "water", label: "供水", icon: <Droplets className="h-4 w-4 text-cyan-600" /> },
- { key: "drain", label: "排水", icon: <Waves className="h-4 w-4 text-emerald-600" /> },
- ] as const
- function useMockOverviewData() {
- // Deterministic mock numbers
- const base = {
- gas: { facilities: 1240, devices: 8420, onlineRate: 96.2, supply: 52.4 },
- water: { facilities: 980, devices: 6230, onlineRate: 97.8, supply: 315.6 },
- drain: { facilities: 760, devices: 4210, onlineRate: 94.1, supply: 288.3 },
- }
- const totalFacilities = base.gas.facilities + base.water.facilities + base.drain.facilities
- const totalDevices = base.gas.devices + base.water.devices + base.drain.devices
- const avgOnline =
- Math.round(((base.gas.onlineRate + base.water.onlineRate + base.drain.onlineRate) / 3) * 10) / 10
- const typeShare = [
- { name: "燃气", value: base.gas.facilities, key: "gas" },
- { name: "供水", value: base.water.facilities, key: "water" },
- { name: "排水", value: base.drain.facilities, key: "drain" },
- ]
- const trend = Array.from({ length: 12 }).map((_, i) => ({
- month: `${i + 1}月`,
- // mock报警/事件趋势
- alarms: Math.round(140 + 30 * Math.sin((i / 12) * Math.PI * 2) + (i % 3) * 8),
- supply: base.water.supply * (0.9 + 0.02 * i), // example: 供水能力趋势
- }))
- return { base, totalFacilities, totalDevices, avgOnline, typeShare, trend }
- }
- const shareConfig = {
- gas: { label: "燃气", color: "hsl(var(--chart-1))" },
- water: { label: "供水", color: "hsl(var(--chart-2))" },
- drain: { label: "排水", color: "hsl(var(--chart-3))" },
- } satisfies ChartConfig
- const trendConfig = {
- alarms: { label: "报警数", color: "hsl(var(--chart-4))" },
- supply: { label: "供水能力", color: "hsl(var(--chart-2))" },
- } satisfies ChartConfig
- export default function OverviewDashboard() {
- const { base, totalFacilities, totalDevices, avgOnline, typeShare, trend } = useMockOverviewData()
- const [loading, setLoading] = useState(false)
- const onlineRadial = useMemo(
- () => [{ name: "在线率", online: avgOnline, fill: "hsl(var(--chart-1))" }],
- [avgOnline],
- )
- return (
- <div className="space-y-8">
- <Section
- title="总体概览"
- description="汇聚燃气、供水、排水等生命线基础设施的底数与运行情况。"
- action={
- <Button
- variant="outline"
- onClick={() => {
- setLoading(true)
- setTimeout(() => setLoading(false), 800)
- }}
- >
- {loading ? "刷新中..." : "刷新数据"}
- </Button>
- }
- >
- <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
- <MetricCard
- title="设施总量"
- value={totalFacilities}
- icon={<Cpu className="h-4 w-4 text-emerald-600" aria-hidden />}
- trend={{ delta: 2.4, label: "同比" }}
- />
- <MetricCard
- title="监测设备"
- value={totalDevices}
- icon={<Activity className="h-4 w-4 text-emerald-600" aria-hidden />}
- trend={{ delta: 1.2, label: "环比" }}
- />
- <MetricCard
- title="平均在线率"
- value={avgOnline}
- suffix="%"
- icon={<Activity className="h-4 w-4 text-emerald-600" aria-hidden />}
- trend={{ delta: 0.6, label: "近7天" }}
- />
- <MetricCard
- title="供水能力"
- value={Math.round(base.water.supply)}
- suffix="万m³/日"
- icon={<Droplets className="h-4 w-4 text-cyan-600" aria-hidden />}
- />
- </div>
- </Section>
- <Section title="设施底数结构" description="构建设施底数指标体系,展示设施类型占比与规模变化。">
- <div className="grid gap-4 lg:grid-cols-3">
- <Card className="lg:col-span-1">
- <CardHeader>
- <CardTitle className="text-sm">设施类型占比</CardTitle>
- </CardHeader>
- <CardContent>
- <ChartContainer config={shareConfig} className="h-[260px]">
- <ResponsiveContainer width="100%" height="100%">
- <PieChart>
- <ChartTooltip content={<ChartTooltipContent />} />
- <ChartLegend content={<ChartLegendContent />} />
- <Pie
- data={typeShare}
- dataKey="value"
- nameKey="name"
- innerRadius={55}
- outerRadius={90}
- paddingAngle={2}
- >
- {typeShare.map((entry) => (
- <Cell key={entry.key} fill={`var(--color-${entry.key})`} />
- ))}
- </Pie>
- </PieChart>
- </ResponsiveContainer>
- </ChartContainer>
- <div className="mt-2 grid grid-cols-3 gap-2 text-center text-xs text-muted-foreground">
- {industries.map((it) => (
- <div key={it.key} className="flex items-center justify-center gap-1">
- {it.icon}
- <span>{it.label}</span>
- </div>
- ))}
- </div>
- </CardContent>
- </Card>
- <Card className="lg:col-span-2">
- <CardHeader>
- <CardTitle className="text-sm">报警趋势与供水能力</CardTitle>
- </CardHeader>
- <CardContent>
- <ChartContainer config={trendConfig} className="h-[260px]">
- <ResponsiveContainer width="100%" height="100%">
- <ComposedTrend data={trend} />
- </ResponsiveContainer>
- </ChartContainer>
- </CardContent>
- </Card>
- </div>
- </Section>
- <Section title="运行稳定性" description="展示在线率等关键稳定性指标。">
- <div className="grid gap-4 md:grid-cols-2">
- <Card>
- <CardHeader>
- <CardTitle className="text-sm">平均在线率</CardTitle>
- </CardHeader>
- <CardContent>
- <ChartContainer
- config={{ online: { label: "在线率", color: "hsl(var(--chart-1))" } }}
- className="mx-auto h-[220px] w-full max-w-[380px]"
- >
- <ResponsiveContainer width="100%" height="100%">
- <RadialBarChart innerRadius="75%" outerRadius="100%" data={[{ name: "在线率", online: avgOnline }]}>
- <RadialBar dataKey="online" cornerRadius={8} fill="var(--color-online)" />
- <ChartTooltip content={<ChartTooltipContent hideLabel />} />
- </RadialBarChart>
- </ResponsiveContainer>
- </ChartContainer>
- <p className="text-center text-xs text-muted-foreground mt-2">目标 ≥ 95%</p>
- </CardContent>
- </Card>
- <Card>
- <CardHeader>
- <CardTitle className="text-sm">设施规模变化(样例)</CardTitle>
- </CardHeader>
- <CardContent>
- <ChartContainer
- config={{ facilities: { label: "设施数量", color: "hsl(var(--chart-3))" } }}
- className="h-[220px]"
- >
- <ResponsiveContainer width="100%" height="100%">
- <BarChart
- data={Array.from({ length: 8 }).map((_, i) => ({
- name: `${i + 1}期`,
- facilities: 2200 + (i % 3) * 120 + Math.round(50 * Math.sin(i)),
- }))}
- >
- <CartesianGrid vertical={false} strokeDasharray="3 3" />
- <XAxis dataKey="name" tickLine={false} axisLine={false} />
- <YAxis tickLine={false} axisLine={false} />
- <ChartTooltip content={<ChartTooltipContent />} />
- <Bar dataKey="facilities" fill="var(--color-facilities)" radius={6} />
- </BarChart>
- </ResponsiveContainer>
- </ChartContainer>
- </CardContent>
- </Card>
- </div>
- </Section>
- </div>
- )
- }
- function ComposedTrend({ data }: { data: Array<{ month: string; alarms: number; supply: number }> }) {
- return (
- <LineChart data={data}>
- <CartesianGrid strokeDasharray="3 3" vertical={false} />
- <XAxis dataKey="month" tickLine={false} axisLine={false} />
- <YAxis yAxisId="left" tickLine={false} axisLine={false} />
- <YAxis yAxisId="right" orientation="right" tickLine={false} axisLine={false} />
- <ChartTooltip content={<ChartTooltipContent />} />
- <Legend />
- <Area
- yAxisId="right"
- type="monotone"
- dataKey="supply"
- fill="var(--color-supply)"
- stroke="var(--color-supply)"
- fillOpacity={0.2}
- />
- <Line yAxisId="left" type="monotone" dataKey="alarms" stroke="var(--color-alarms)" strokeWidth={2} dot={false} />
- </LineChart>
- )
- }
|