alerts-dashboard.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. "use client"
  2. import {useMemo, useState} from "react"
  3. import Section from "@/components/shared/section"
  4. import {Button} from "@/components/ui/button"
  5. import {Badge} from "@/components/ui/badge"
  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. Bar,
  17. BarChart,
  18. CartesianGrid,
  19. Cell,
  20. Line,
  21. LineChart,
  22. Pie,
  23. PieChart,
  24. ResponsiveContainer,
  25. XAxis,
  26. YAxis,
  27. } from "recharts"
  28. import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table"
  29. import {Dialog, DialogContent, DialogHeader, DialogTitle} from "@/components/ui/dialog"
  30. import {Separator} from "@/components/ui/separator"
  31. import {AlarmClock, ShieldCheck} from 'lucide-react'
  32. type Warning = {
  33. id: string
  34. type: "泄漏" | "水压异常" | "液位过高" | "阀门故障"
  35. industry: "燃气" | "供水" | "排水"
  36. severity: "高" | "中" | "低"
  37. district: string
  38. time: string
  39. status: "待处置" | "处置中" | "已完成"
  40. }
  41. function useWarnings() {
  42. const now = new Date()
  43. const items: Warning[] = Array.from({ length: 12 }).map((_, i) => {
  44. const types: Warning["type"][] = ["泄漏", "水压异常", "液位过高", "阀门故障"]
  45. const industries: Warning["industry"][] = ["燃气", "供水", "排水"]
  46. const severities: Warning["severity"][] = ["高", "中", "低"]
  47. const statuses: Warning["status"][] = ["待处置", "处置中", "已完成"]
  48. const t = new Date(now.getTime() - i * 36 * 60 * 1000)
  49. return {
  50. id: `ALM-${1000 + i}`,
  51. type: types[i % types.length]!,
  52. industry: industries[(i + 1) % industries.length]!,
  53. severity: severities[i % severities.length]!,
  54. district: ["天河区", "越秀区", "黄埔区", "海珠区"][i % 4]!,
  55. time: t.toLocaleString(),
  56. status: statuses[i % statuses.length]!,
  57. }
  58. })
  59. return items
  60. }
  61. const trendConfig = {
  62. alarms: { label: "报警数", color: "hsl(var(--chart-5))" },
  63. } satisfies ChartConfig
  64. export default function AlertsDashboard() {
  65. const warnings = useWarnings()
  66. const [selected, setSelected] = useState<Warning | null>(null)
  67. const trend = useMemo(
  68. () =>
  69. Array.from({ length: 10 }).map((_, i) => ({
  70. name: `${i + 1}日`,
  71. alarms: Math.round(80 + 20 * Math.sin(i / 2) + (i % 3) * 6),
  72. })),
  73. [],
  74. )
  75. const byIndustry = useMemo(() => {
  76. const data = [
  77. { name: "燃气", value: 0 },
  78. { name: "供水", value: 0 },
  79. { name: "排水", value: 0 },
  80. ]
  81. warnings.forEach((w) => {
  82. const idx = data.findIndex((d) => d.name === w.industry)
  83. data[idx]!.value++
  84. })
  85. return data
  86. }, [warnings])
  87. const efficiency = { correct: 86, avgMinutes: 42 }
  88. return (
  89. <div className="space-y-8">
  90. <Section
  91. title="当前预警"
  92. description="综合展示总体预警处置情况,支持与一张图联动展示具体预警事件信息。"
  93. action={<Button variant="outline" onClick={() => location.reload()}>模拟刷新</Button>}
  94. >
  95. <div className="grid gap-4 lg:grid-cols-3">
  96. <Card className="lg:col-span-2">
  97. <CardHeader>
  98. <CardTitle className="text-sm">预警列表</CardTitle>
  99. </CardHeader>
  100. <CardContent className="overflow-x-auto">
  101. <Table>
  102. <TableHeader>
  103. <TableRow>
  104. <TableHead>编号</TableHead>
  105. <TableHead>类型</TableHead>
  106. <TableHead>行业</TableHead>
  107. <TableHead>等级</TableHead>
  108. <TableHead>区域</TableHead>
  109. <TableHead>时间</TableHead>
  110. <TableHead>状态</TableHead>
  111. </TableRow>
  112. </TableHeader>
  113. <TableBody>
  114. {warnings.map((w) => (
  115. <TableRow key={w.id} className="cursor-pointer" onClick={() => setSelected(w)}>
  116. <TableCell className="font-medium">{w.id}</TableCell>
  117. <TableCell>{w.type}</TableCell>
  118. <TableCell>{w.industry}</TableCell>
  119. <TableCell>
  120. <Badge
  121. className={
  122. w.severity === "高"
  123. ? "bg-rose-500 hover:bg-rose-500"
  124. : w.severity === "中"
  125. ? "bg-amber-500 hover:bg-amber-500"
  126. : "bg-emerald-500 hover:bg-emerald-500"
  127. }
  128. >
  129. {w.severity}
  130. </Badge>
  131. </TableCell>
  132. <TableCell>{w.district}</TableCell>
  133. <TableCell className="whitespace-nowrap">{w.time}</TableCell>
  134. <TableCell>
  135. <Badge variant="outline">{w.status}</Badge>
  136. </TableCell>
  137. </TableRow>
  138. ))}
  139. </TableBody>
  140. </Table>
  141. </CardContent>
  142. </Card>
  143. <Card>
  144. <CardHeader>
  145. <CardTitle className="text-sm">近10日报警趋势</CardTitle>
  146. </CardHeader>
  147. <CardContent>
  148. <ChartContainer config={trendConfig} className="h-[240px]">
  149. <ResponsiveContainer width="100%" height="100%">
  150. <LineChart data={trend}>
  151. <CartesianGrid strokeDasharray="3 3" vertical={false} />
  152. <XAxis dataKey="name" tickLine={false} axisLine={false} />
  153. <YAxis tickLine={false} axisLine={false} />
  154. <ChartTooltip content={<ChartTooltipContent />} />
  155. <Line type="monotone" dataKey="alarms" stroke="var(--color-alarms)" strokeWidth={2} dot={false} />
  156. </LineChart>
  157. </ResponsiveContainer>
  158. </ChartContainer>
  159. </CardContent>
  160. </Card>
  161. </div>
  162. </Section>
  163. <Section title="历史分析" description="按行业统计预警数量,分析处置效率变化趋势。">
  164. <div className="grid gap-4 lg:grid-cols-3">
  165. <Card>
  166. <CardHeader>
  167. <CardTitle className="text-sm">按行业分布</CardTitle>
  168. </CardHeader>
  169. <CardContent>
  170. <ChartContainer
  171. config={{
  172. gas: { label: "燃气", color: "hsl(var(--chart-1))" },
  173. water: { label: "供水", color: "hsl(var(--chart-2))" },
  174. drain: { label: "排水", color: "hsl(var(--chart-3))" },
  175. }}
  176. className="h-[240px]"
  177. >
  178. <ResponsiveContainer width="100%" height="100%">
  179. <PieChart>
  180. <ChartTooltip content={<ChartTooltipContent />} />
  181. <ChartLegend content={<ChartLegendContent />} />
  182. <Pie data={byIndustry} dataKey="value" nameKey="name" innerRadius={45} outerRadius={80}>
  183. {byIndustry.map((d) => (
  184. <Cell
  185. key={d.name}
  186. fill={
  187. d.name === "燃气"
  188. ? "var(--color-gas)"
  189. : d.name === "供水"
  190. ? "var(--color-water)"
  191. : "var(--color-drain)"
  192. }
  193. />
  194. ))}
  195. </Pie>
  196. </PieChart>
  197. </ResponsiveContainer>
  198. </ChartContainer>
  199. </CardContent>
  200. </Card>
  201. <Card className="lg:col-span-2">
  202. <CardHeader>
  203. <CardTitle className="text-sm">平均处置时效(分钟)</CardTitle>
  204. </CardHeader>
  205. <CardContent>
  206. <ChartContainer
  207. config={{ minutes: { label: "分钟", color: "hsl(var(--chart-4))" } }}
  208. className="h-[240px]"
  209. >
  210. <ResponsiveContainer width="100%" height="100%">
  211. <BarChart
  212. data={Array.from({ length: 8 }).map((_, i) => ({
  213. name: `${i + 1}周`,
  214. minutes: Math.round(50 - i * 2 + (i % 3) * 4),
  215. }))}
  216. >
  217. <CartesianGrid vertical={false} strokeDasharray="3 3" />
  218. <XAxis dataKey="name" tickLine={false} axisLine={false} />
  219. <YAxis tickLine={false} axisLine={false} />
  220. <ChartTooltip content={<ChartTooltipContent />} />
  221. <Bar dataKey="minutes" radius={6} fill="var(--color-minutes)" />
  222. </BarChart>
  223. </ResponsiveContainer>
  224. </ChartContainer>
  225. </CardContent>
  226. </Card>
  227. </div>
  228. </Section>
  229. <Section title="预警效率" description="统计正确预警数量,展示整体预警效率。">
  230. <div className="grid gap-4 md:grid-cols-2">
  231. <Card>
  232. <CardHeader>
  233. <CardTitle className="text-sm">正确预警比例</CardTitle>
  234. </CardHeader>
  235. <CardContent className="flex items-center gap-6">
  236. <div className="relative grid place-items-center">
  237. <svg width="140" height="140" viewBox="0 0 36 36" aria-label="正确预警比例">
  238. <path
  239. d="M18 2.0845
  240. a 15.9155 15.9155 0 0 1 0 31.831
  241. a 15.9155 15.9155 0 0 1 0 -31.831"
  242. fill="none"
  243. stroke="hsl(var(--muted-foreground))"
  244. strokeWidth="2"
  245. opacity="0.2"
  246. />
  247. <path
  248. d="M18 2.0845
  249. a 15.9155 15.9155 0 0 1 0 31.831
  250. a 15.9155 15.9155 0 0 1 0 -31.831"
  251. fill="none"
  252. stroke="hsl(var(--chart-1))"
  253. strokeWidth="2.5"
  254. strokeDasharray={`${efficiency.correct}, 100`}
  255. strokeLinecap="round"
  256. />
  257. <text x="18" y="20.35" className="fill-foreground" textAnchor="middle" fontSize="6">
  258. {efficiency.correct}%
  259. </text>
  260. </svg>
  261. </div>
  262. <div className="text-sm">
  263. <div className="flex items-center gap-2">
  264. <ShieldCheck className="h-4 w-4 text-emerald-600" />
  265. 正确预警
  266. </div>
  267. <p className="text-muted-foreground mt-1">
  268. 结合人工复核等处置数据,统计系统预警准确度。
  269. </p>
  270. </div>
  271. </CardContent>
  272. </Card>
  273. <Card>
  274. <CardHeader>
  275. <CardTitle className="text-sm">平均处置用时</CardTitle>
  276. </CardHeader>
  277. <CardContent className="flex items-center gap-6">
  278. <AlarmClock className="h-8 w-8 text-emerald-600" />
  279. <div>
  280. <div className="text-2xl font-semibold">{efficiency.avgMinutes} 分钟</div>
  281. <p className="text-xs text-muted-foreground mt-1">较上周提升 8%</p>
  282. </div>
  283. </CardContent>
  284. </Card>
  285. </div>
  286. </Section>
  287. <Section title="预警分析" description="按类型与成因维度分析预警分布情况。">
  288. <div className="grid gap-4 md:grid-cols-2">
  289. <Card>
  290. <CardHeader>
  291. <CardTitle className="text-sm">按类型(样例)</CardTitle>
  292. </CardHeader>
  293. <CardContent>
  294. <ChartContainer
  295. config={{
  296. gas: { label: "泄漏", color: "hsl(var(--chart-1))" },
  297. water: { label: "水压异常", color: "hsl(var(--chart-2))" },
  298. tank: { label: "液位过高", color: "hsl(var(--chart-3))" },
  299. valve: { label: "阀门故障", color: "hsl(var(--chart-4))" },
  300. }}
  301. className="h-[240px]"
  302. >
  303. <ResponsiveContainer width="100%" height="100%">
  304. <BarChart
  305. data={[
  306. { name: "一月", 泄漏: 42, 水压异常: 58, 液位过高: 32, 阀门故障: 20 },
  307. { name: "二月", 泄漏: 36, 水压异常: 62, 液位过高: 27, 阀门故障: 24 },
  308. { name: "三月", 泄漏: 50, 水压异常: 54, 液位过高: 31, 阀门故障: 22 },
  309. ]}
  310. >
  311. <CartesianGrid vertical={false} strokeDasharray="3 3" />
  312. <XAxis dataKey="name" tickLine={false} axisLine={false} />
  313. <YAxis tickLine={false} axisLine={false} />
  314. <ChartTooltip content={<ChartTooltipContent />} />
  315. <Bar dataKey="泄漏" stackId="a" fill="var(--color-gas)" radius={4} />
  316. <Bar dataKey="水压异常" stackId="a" fill="var(--color-water)" radius={4} />
  317. <Bar dataKey="液位过高" stackId="a" fill="var(--color-tank)" radius={4} />
  318. <Bar dataKey="阀门故障" stackId="a" fill="var(--color-valve)" radius={4} />
  319. </BarChart>
  320. </ResponsiveContainer>
  321. </ChartContainer>
  322. </CardContent>
  323. </Card>
  324. <Card>
  325. <CardHeader>
  326. <CardTitle className="text-sm">成因分布(样例)</CardTitle>
  327. </CardHeader>
  328. <CardContent>
  329. <ChartContainer
  330. config={{
  331. human: { label: "人为", color: "hsl(var(--chart-5))" },
  332. aging: { label: "老化", color: "hsl(var(--chart-3))" },
  333. env: { label: "环境", color: "hsl(var(--chart-2))" },
  334. }}
  335. className="h-[240px]"
  336. >
  337. <ResponsiveContainer width="100%" height="100%">
  338. <PieChart>
  339. <ChartTooltip content={<ChartTooltipContent />} />
  340. <ChartLegend content={<ChartLegendContent />} />
  341. <Pie
  342. data={[
  343. { name: "人为", value: 36 },
  344. { name: "老化", value: 44 },
  345. { name: "环境", value: 20 },
  346. ]}
  347. dataKey="value"
  348. nameKey="name"
  349. innerRadius={45}
  350. outerRadius={80}
  351. paddingAngle={2}
  352. >
  353. <Cell key="人为" fill="var(--color-human)" />
  354. <Cell key="老化" fill="var(--color-aging)" />
  355. <Cell key="环境" fill="var(--color-env)" />
  356. </Pie>
  357. </PieChart>
  358. </ResponsiveContainer>
  359. </ChartContainer>
  360. </CardContent>
  361. </Card>
  362. </div>
  363. </Section>
  364. <WarningDialog warning={selected} onOpenChange={(open) => !open && setSelected(null)} />
  365. </div>
  366. )
  367. }
  368. function WarningDialog({
  369. warning,
  370. onOpenChange,
  371. }: {
  372. warning: Warning | null
  373. onOpenChange: (open: boolean) => void
  374. }) {
  375. return (
  376. <Dialog open={!!warning} onOpenChange={onOpenChange}>
  377. <DialogContent className="max-w-2xl">
  378. <DialogHeader>
  379. <DialogTitle>预警详情</DialogTitle>
  380. </DialogHeader>
  381. {warning ? (
  382. <div className="text-sm">
  383. <div className="grid grid-cols-2 gap-3">
  384. <div>
  385. <span className="text-muted-foreground">编号:</span>
  386. {warning.id}
  387. </div>
  388. <div>
  389. <span className="text-muted-foreground">时间:</span>
  390. {warning.time}
  391. </div>
  392. <div>
  393. <span className="text-muted-foreground">类型:</span>
  394. {warning.type}
  395. </div>
  396. <div>
  397. <span className="text-muted-foreground">行业:</span>
  398. {warning.industry}
  399. </div>
  400. <div>
  401. <span className="text-muted-foreground">等级:</span>
  402. <Badge
  403. className={
  404. warning.severity === "高"
  405. ? "bg-rose-500 hover:bg-rose-500"
  406. : warning.severity === "中"
  407. ? "bg-amber-500 hover:bg-amber-500"
  408. : "bg-emerald-500 hover:bg-emerald-500"
  409. }
  410. >
  411. {warning.severity}
  412. </Badge>
  413. </div>
  414. <div>
  415. <span className="text-muted-foreground">状态:</span>
  416. <Badge variant="outline">{warning.status}</Badge>
  417. </div>
  418. <div className="col-span-2">
  419. <span className="text-muted-foreground">区域:</span>
  420. {warning.district}
  421. </div>
  422. </div>
  423. <Separator className="my-4" />
  424. <div className="rounded-md border p-3">
  425. <div className="text-xs text-muted-foreground mb-2">定位(示意)</div>
  426. <div className="relative h-48 w-full overflow-hidden rounded">
  427. <svg viewBox="0 0 100 100" className="absolute inset-0 h-full w-full">
  428. <rect width="100" height="100" fill="hsl(var(--muted))" opacity="0.3" />
  429. <path d="M 0 50 L 100 50" stroke="hsl(var(--border))" strokeWidth="1" />
  430. <circle cx="62" cy="42" r="3" fill="hsl(var(--primary))" />
  431. </svg>
  432. </div>
  433. </div>
  434. </div>
  435. ) : null}
  436. </DialogContent>
  437. </Dialog>
  438. )
  439. }