overview-dashboard.tsx 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. "use client"
  2. import {Activity, Cpu, Droplets, Flame, Waves} from 'lucide-react'
  3. import MetricCard from "@/components/shared/metric-card"
  4. import Section from "@/components/shared/section"
  5. import {Button} from "@/components/ui/button"
  6. import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card"
  7. import {
  8. type ChartConfig,
  9. ChartContainer,
  10. ChartLegend,
  11. ChartLegendContent,
  12. ChartTooltip,
  13. ChartTooltipContent,
  14. } from "@/components/ui/chart"
  15. import {
  16. Area,
  17. Bar,
  18. BarChart,
  19. CartesianGrid,
  20. Legend,
  21. Line,
  22. LineChart,
  23. Pie,
  24. PieChart,
  25. RadialBar,
  26. RadialBarChart,
  27. ResponsiveContainer,
  28. XAxis,
  29. YAxis,
  30. } from "recharts"
  31. import {useMemo, useState} from "react"
  32. const industries = [
  33. { key: "gas", label: "燃气", icon: <Flame className="h-4 w-4 text-amber-600" /> },
  34. { key: "water", label: "供水", icon: <Droplets className="h-4 w-4 text-cyan-600" /> },
  35. { key: "drain", label: "排水", icon: <Waves className="h-4 w-4 text-emerald-600" /> },
  36. ] as const
  37. function useMockOverviewData() {
  38. // Deterministic mock numbers
  39. const base = {
  40. gas: { facilities: 1240, devices: 8420, onlineRate: 96.2, supply: 52.4 },
  41. water: { facilities: 980, devices: 6230, onlineRate: 97.8, supply: 315.6 },
  42. drain: { facilities: 760, devices: 4210, onlineRate: 94.1, supply: 288.3 },
  43. }
  44. const totalFacilities = base.gas.facilities + base.water.facilities + base.drain.facilities
  45. const totalDevices = base.gas.devices + base.water.devices + base.drain.devices
  46. const avgOnline =
  47. Math.round(((base.gas.onlineRate + base.water.onlineRate + base.drain.onlineRate) / 3) * 10) / 10
  48. const typeShare = [
  49. { name: "燃气", value: base.gas.facilities, key: "gas" },
  50. { name: "供水", value: base.water.facilities, key: "water" },
  51. { name: "排水", value: base.drain.facilities, key: "drain" },
  52. ]
  53. const trend = Array.from({ length: 12 }).map((_, i) => ({
  54. month: `${i + 1}月`,
  55. // mock报警/事件趋势
  56. alarms: Math.round(140 + 30 * Math.sin((i / 12) * Math.PI * 2) + (i % 3) * 8),
  57. supply: base.water.supply * (0.9 + 0.02 * i), // example: 供水能力趋势
  58. }))
  59. return { base, totalFacilities, totalDevices, avgOnline, typeShare, trend }
  60. }
  61. const shareConfig = {
  62. gas: { label: "燃气", color: "hsl(var(--chart-1))" },
  63. water: { label: "供水", color: "hsl(var(--chart-2))" },
  64. drain: { label: "排水", color: "hsl(var(--chart-3))" },
  65. } satisfies ChartConfig
  66. const trendConfig = {
  67. alarms: { label: "报警数", color: "hsl(var(--chart-4))" },
  68. supply: { label: "供水能力", color: "hsl(var(--chart-2))" },
  69. } satisfies ChartConfig
  70. export default function OverviewDashboard() {
  71. const { base, totalFacilities, totalDevices, avgOnline, typeShare, trend } = useMockOverviewData()
  72. const [loading, setLoading] = useState(false)
  73. const onlineRadial = useMemo(
  74. () => [{ name: "在线率", online: avgOnline, fill: "hsl(var(--chart-1))" }],
  75. [avgOnline],
  76. )
  77. return (
  78. <div className="space-y-8">
  79. <Section
  80. title="总体概览"
  81. description="汇聚燃气、供水、排水等生命线基础设施的底数与运行情况。"
  82. action={
  83. <Button
  84. variant="outline"
  85. onClick={() => {
  86. setLoading(true)
  87. setTimeout(() => setLoading(false), 800)
  88. }}
  89. >
  90. {loading ? "刷新中..." : "刷新数据"}
  91. </Button>
  92. }
  93. >
  94. <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
  95. <MetricCard
  96. title="设施总量"
  97. value={totalFacilities}
  98. icon={<Cpu className="h-4 w-4 text-emerald-600" aria-hidden />}
  99. trend={{ delta: 2.4, label: "同比" }}
  100. />
  101. <MetricCard
  102. title="监测设备"
  103. value={totalDevices}
  104. icon={<Activity className="h-4 w-4 text-emerald-600" aria-hidden />}
  105. trend={{ delta: 1.2, label: "环比" }}
  106. />
  107. <MetricCard
  108. title="平均在线率"
  109. value={avgOnline}
  110. suffix="%"
  111. icon={<Activity className="h-4 w-4 text-emerald-600" aria-hidden />}
  112. trend={{ delta: 0.6, label: "近7天" }}
  113. />
  114. <MetricCard
  115. title="供水能力"
  116. value={Math.round(base.water.supply)}
  117. suffix="万m³/日"
  118. icon={<Droplets className="h-4 w-4 text-cyan-600" aria-hidden />}
  119. />
  120. </div>
  121. </Section>
  122. <Section title="设施底数结构" description="构建设施底数指标体系,展示设施类型占比与规模变化。">
  123. <div className="grid gap-4 lg:grid-cols-3">
  124. <Card className="lg:col-span-1">
  125. <CardHeader>
  126. <CardTitle className="text-sm">设施类型占比</CardTitle>
  127. </CardHeader>
  128. <CardContent>
  129. <ChartContainer config={shareConfig} className="h-[260px]">
  130. <ResponsiveContainer width="100%" height="100%">
  131. <PieChart>
  132. <ChartTooltip content={<ChartTooltipContent />} />
  133. <ChartLegend content={<ChartLegendContent />} />
  134. <Pie
  135. data={typeShare}
  136. dataKey="value"
  137. nameKey="name"
  138. innerRadius={55}
  139. outerRadius={90}
  140. paddingAngle={2}
  141. >
  142. {typeShare.map((entry) => (
  143. <Cell key={entry.key} fill={`var(--color-${entry.key})`} />
  144. ))}
  145. </Pie>
  146. </PieChart>
  147. </ResponsiveContainer>
  148. </ChartContainer>
  149. <div className="mt-2 grid grid-cols-3 gap-2 text-center text-xs text-muted-foreground">
  150. {industries.map((it) => (
  151. <div key={it.key} className="flex items-center justify-center gap-1">
  152. {it.icon}
  153. <span>{it.label}</span>
  154. </div>
  155. ))}
  156. </div>
  157. </CardContent>
  158. </Card>
  159. <Card className="lg:col-span-2">
  160. <CardHeader>
  161. <CardTitle className="text-sm">报警趋势与供水能力</CardTitle>
  162. </CardHeader>
  163. <CardContent>
  164. <ChartContainer config={trendConfig} className="h-[260px]">
  165. <ResponsiveContainer width="100%" height="100%">
  166. <ComposedTrend data={trend} />
  167. </ResponsiveContainer>
  168. </ChartContainer>
  169. </CardContent>
  170. </Card>
  171. </div>
  172. </Section>
  173. <Section title="运行稳定性" description="展示在线率等关键稳定性指标。">
  174. <div className="grid gap-4 md:grid-cols-2">
  175. <Card>
  176. <CardHeader>
  177. <CardTitle className="text-sm">平均在线率</CardTitle>
  178. </CardHeader>
  179. <CardContent>
  180. <ChartContainer
  181. config={{ online: { label: "在线率", color: "hsl(var(--chart-1))" } }}
  182. className="mx-auto h-[220px] w-full max-w-[380px]"
  183. >
  184. <ResponsiveContainer width="100%" height="100%">
  185. <RadialBarChart innerRadius="75%" outerRadius="100%" data={[{ name: "在线率", online: avgOnline }]}>
  186. <RadialBar dataKey="online" cornerRadius={8} fill="var(--color-online)" />
  187. <ChartTooltip content={<ChartTooltipContent hideLabel />} />
  188. </RadialBarChart>
  189. </ResponsiveContainer>
  190. </ChartContainer>
  191. <p className="text-center text-xs text-muted-foreground mt-2">目标 ≥ 95%</p>
  192. </CardContent>
  193. </Card>
  194. <Card>
  195. <CardHeader>
  196. <CardTitle className="text-sm">设施规模变化(样例)</CardTitle>
  197. </CardHeader>
  198. <CardContent>
  199. <ChartContainer
  200. config={{ facilities: { label: "设施数量", color: "hsl(var(--chart-3))" } }}
  201. className="h-[220px]"
  202. >
  203. <ResponsiveContainer width="100%" height="100%">
  204. <BarChart
  205. data={Array.from({ length: 8 }).map((_, i) => ({
  206. name: `${i + 1}期`,
  207. facilities: 2200 + (i % 3) * 120 + Math.round(50 * Math.sin(i)),
  208. }))}
  209. >
  210. <CartesianGrid vertical={false} strokeDasharray="3 3" />
  211. <XAxis dataKey="name" tickLine={false} axisLine={false} />
  212. <YAxis tickLine={false} axisLine={false} />
  213. <ChartTooltip content={<ChartTooltipContent />} />
  214. <Bar dataKey="facilities" fill="var(--color-facilities)" radius={6} />
  215. </BarChart>
  216. </ResponsiveContainer>
  217. </ChartContainer>
  218. </CardContent>
  219. </Card>
  220. </div>
  221. </Section>
  222. </div>
  223. )
  224. }
  225. function ComposedTrend({ data }: { data: Array<{ month: string; alarms: number; supply: number }> }) {
  226. return (
  227. <LineChart data={data}>
  228. <CartesianGrid strokeDasharray="3 3" vertical={false} />
  229. <XAxis dataKey="month" tickLine={false} axisLine={false} />
  230. <YAxis yAxisId="left" tickLine={false} axisLine={false} />
  231. <YAxis yAxisId="right" orientation="right" tickLine={false} axisLine={false} />
  232. <ChartTooltip content={<ChartTooltipContent />} />
  233. <Legend />
  234. <Area
  235. yAxisId="right"
  236. type="monotone"
  237. dataKey="supply"
  238. fill="var(--color-supply)"
  239. stroke="var(--color-supply)"
  240. fillOpacity={0.2}
  241. />
  242. <Line yAxisId="left" type="monotone" dataKey="alarms" stroke="var(--color-alarms)" strokeWidth={2} dot={false} />
  243. </LineChart>
  244. )
  245. }