| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457 |
- "use client"
- import {useMemo, useState} from "react"
- import Section from "@/components/shared/section"
- import {Button} from "@/components/ui/button"
- import {Badge} from "@/components/ui/badge"
- import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card"
- import {
- type ChartConfig,
- ChartContainer,
- ChartLegend,
- ChartLegendContent,
- ChartTooltip,
- ChartTooltipContent,
- } from "@/components/ui/chart"
- import {
- Bar,
- BarChart,
- CartesianGrid,
- Cell,
- Line,
- LineChart,
- Pie,
- PieChart,
- ResponsiveContainer,
- XAxis,
- YAxis,
- } from "recharts"
- import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table"
- import {Dialog, DialogContent, DialogHeader, DialogTitle} from "@/components/ui/dialog"
- import {Separator} from "@/components/ui/separator"
- import {AlarmClock, ShieldCheck} from 'lucide-react'
- type Warning = {
- id: string
- type: "泄漏" | "水压异常" | "液位过高" | "阀门故障"
- industry: "燃气" | "供水" | "排水"
- severity: "高" | "中" | "低"
- district: string
- time: string
- status: "待处置" | "处置中" | "已完成"
- }
- function useWarnings() {
- const now = new Date()
- const items: Warning[] = Array.from({ length: 12 }).map((_, i) => {
- const types: Warning["type"][] = ["泄漏", "水压异常", "液位过高", "阀门故障"]
- const industries: Warning["industry"][] = ["燃气", "供水", "排水"]
- const severities: Warning["severity"][] = ["高", "中", "低"]
- const statuses: Warning["status"][] = ["待处置", "处置中", "已完成"]
- const t = new Date(now.getTime() - i * 36 * 60 * 1000)
- return {
- id: `ALM-${1000 + i}`,
- type: types[i % types.length]!,
- industry: industries[(i + 1) % industries.length]!,
- severity: severities[i % severities.length]!,
- district: ["天河区", "越秀区", "黄埔区", "海珠区"][i % 4]!,
- time: t.toLocaleString(),
- status: statuses[i % statuses.length]!,
- }
- })
- return items
- }
- const trendConfig = {
- alarms: { label: "报警数", color: "hsl(var(--chart-5))" },
- } satisfies ChartConfig
- export default function AlertsDashboard() {
- const warnings = useWarnings()
- const [selected, setSelected] = useState<Warning | null>(null)
- const trend = useMemo(
- () =>
- Array.from({ length: 10 }).map((_, i) => ({
- name: `${i + 1}日`,
- alarms: Math.round(80 + 20 * Math.sin(i / 2) + (i % 3) * 6),
- })),
- [],
- )
- const byIndustry = useMemo(() => {
- const data = [
- { name: "燃气", value: 0 },
- { name: "供水", value: 0 },
- { name: "排水", value: 0 },
- ]
- warnings.forEach((w) => {
- const idx = data.findIndex((d) => d.name === w.industry)
- data[idx]!.value++
- })
- return data
- }, [warnings])
- const efficiency = { correct: 86, avgMinutes: 42 }
- return (
- <div className="space-y-8">
- <Section
- title="当前预警"
- description="综合展示总体预警处置情况,支持与一张图联动展示具体预警事件信息。"
- action={<Button variant="outline" onClick={() => location.reload()}>模拟刷新</Button>}
- >
- <div className="grid gap-4 lg:grid-cols-3">
- <Card className="lg:col-span-2">
- <CardHeader>
- <CardTitle className="text-sm">预警列表</CardTitle>
- </CardHeader>
- <CardContent className="overflow-x-auto">
- <Table>
- <TableHeader>
- <TableRow>
- <TableHead>编号</TableHead>
- <TableHead>类型</TableHead>
- <TableHead>行业</TableHead>
- <TableHead>等级</TableHead>
- <TableHead>区域</TableHead>
- <TableHead>时间</TableHead>
- <TableHead>状态</TableHead>
- </TableRow>
- </TableHeader>
- <TableBody>
- {warnings.map((w) => (
- <TableRow key={w.id} className="cursor-pointer" onClick={() => setSelected(w)}>
- <TableCell className="font-medium">{w.id}</TableCell>
- <TableCell>{w.type}</TableCell>
- <TableCell>{w.industry}</TableCell>
- <TableCell>
- <Badge
- className={
- w.severity === "高"
- ? "bg-rose-500 hover:bg-rose-500"
- : w.severity === "中"
- ? "bg-amber-500 hover:bg-amber-500"
- : "bg-emerald-500 hover:bg-emerald-500"
- }
- >
- {w.severity}
- </Badge>
- </TableCell>
- <TableCell>{w.district}</TableCell>
- <TableCell className="whitespace-nowrap">{w.time}</TableCell>
- <TableCell>
- <Badge variant="outline">{w.status}</Badge>
- </TableCell>
- </TableRow>
- ))}
- </TableBody>
- </Table>
- </CardContent>
- </Card>
- <Card>
- <CardHeader>
- <CardTitle className="text-sm">近10日报警趋势</CardTitle>
- </CardHeader>
- <CardContent>
- <ChartContainer config={trendConfig} className="h-[240px]">
- <ResponsiveContainer width="100%" height="100%">
- <LineChart data={trend}>
- <CartesianGrid strokeDasharray="3 3" vertical={false} />
- <XAxis dataKey="name" tickLine={false} axisLine={false} />
- <YAxis tickLine={false} axisLine={false} />
- <ChartTooltip content={<ChartTooltipContent />} />
- <Line type="monotone" dataKey="alarms" stroke="var(--color-alarms)" strokeWidth={2} dot={false} />
- </LineChart>
- </ResponsiveContainer>
- </ChartContainer>
- </CardContent>
- </Card>
- </div>
- </Section>
- <Section title="历史分析" description="按行业统计预警数量,分析处置效率变化趋势。">
- <div className="grid gap-4 lg:grid-cols-3">
- <Card>
- <CardHeader>
- <CardTitle className="text-sm">按行业分布</CardTitle>
- </CardHeader>
- <CardContent>
- <ChartContainer
- config={{
- gas: { label: "燃气", color: "hsl(var(--chart-1))" },
- water: { label: "供水", color: "hsl(var(--chart-2))" },
- drain: { label: "排水", color: "hsl(var(--chart-3))" },
- }}
- className="h-[240px]"
- >
- <ResponsiveContainer width="100%" height="100%">
- <PieChart>
- <ChartTooltip content={<ChartTooltipContent />} />
- <ChartLegend content={<ChartLegendContent />} />
- <Pie data={byIndustry} dataKey="value" nameKey="name" innerRadius={45} outerRadius={80}>
- {byIndustry.map((d) => (
- <Cell
- key={d.name}
- fill={
- d.name === "燃气"
- ? "var(--color-gas)"
- : d.name === "供水"
- ? "var(--color-water)"
- : "var(--color-drain)"
- }
- />
- ))}
- </Pie>
- </PieChart>
- </ResponsiveContainer>
- </ChartContainer>
- </CardContent>
- </Card>
- <Card className="lg:col-span-2">
- <CardHeader>
- <CardTitle className="text-sm">平均处置时效(分钟)</CardTitle>
- </CardHeader>
- <CardContent>
- <ChartContainer
- config={{ minutes: { label: "分钟", color: "hsl(var(--chart-4))" } }}
- className="h-[240px]"
- >
- <ResponsiveContainer width="100%" height="100%">
- <BarChart
- data={Array.from({ length: 8 }).map((_, i) => ({
- name: `${i + 1}周`,
- minutes: Math.round(50 - i * 2 + (i % 3) * 4),
- }))}
- >
- <CartesianGrid vertical={false} strokeDasharray="3 3" />
- <XAxis dataKey="name" tickLine={false} axisLine={false} />
- <YAxis tickLine={false} axisLine={false} />
- <ChartTooltip content={<ChartTooltipContent />} />
- <Bar dataKey="minutes" radius={6} fill="var(--color-minutes)" />
- </BarChart>
- </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 className="flex items-center gap-6">
- <div className="relative grid place-items-center">
- <svg width="140" height="140" viewBox="0 0 36 36" aria-label="正确预警比例">
- <path
- d="M18 2.0845
- a 15.9155 15.9155 0 0 1 0 31.831
- a 15.9155 15.9155 0 0 1 0 -31.831"
- fill="none"
- stroke="hsl(var(--muted-foreground))"
- strokeWidth="2"
- opacity="0.2"
- />
- <path
- d="M18 2.0845
- a 15.9155 15.9155 0 0 1 0 31.831
- a 15.9155 15.9155 0 0 1 0 -31.831"
- fill="none"
- stroke="hsl(var(--chart-1))"
- strokeWidth="2.5"
- strokeDasharray={`${efficiency.correct}, 100`}
- strokeLinecap="round"
- />
- <text x="18" y="20.35" className="fill-foreground" textAnchor="middle" fontSize="6">
- {efficiency.correct}%
- </text>
- </svg>
- </div>
- <div className="text-sm">
- <div className="flex items-center gap-2">
- <ShieldCheck className="h-4 w-4 text-emerald-600" />
- 正确预警
- </div>
- <p className="text-muted-foreground mt-1">
- 结合人工复核等处置数据,统计系统预警准确度。
- </p>
- </div>
- </CardContent>
- </Card>
- <Card>
- <CardHeader>
- <CardTitle className="text-sm">平均处置用时</CardTitle>
- </CardHeader>
- <CardContent className="flex items-center gap-6">
- <AlarmClock className="h-8 w-8 text-emerald-600" />
- <div>
- <div className="text-2xl font-semibold">{efficiency.avgMinutes} 分钟</div>
- <p className="text-xs text-muted-foreground mt-1">较上周提升 8%</p>
- </div>
- </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={{
- gas: { label: "泄漏", color: "hsl(var(--chart-1))" },
- water: { label: "水压异常", color: "hsl(var(--chart-2))" },
- tank: { label: "液位过高", color: "hsl(var(--chart-3))" },
- valve: { label: "阀门故障", color: "hsl(var(--chart-4))" },
- }}
- className="h-[240px]"
- >
- <ResponsiveContainer width="100%" height="100%">
- <BarChart
- data={[
- { name: "一月", 泄漏: 42, 水压异常: 58, 液位过高: 32, 阀门故障: 20 },
- { name: "二月", 泄漏: 36, 水压异常: 62, 液位过高: 27, 阀门故障: 24 },
- { name: "三月", 泄漏: 50, 水压异常: 54, 液位过高: 31, 阀门故障: 22 },
- ]}
- >
- <CartesianGrid vertical={false} strokeDasharray="3 3" />
- <XAxis dataKey="name" tickLine={false} axisLine={false} />
- <YAxis tickLine={false} axisLine={false} />
- <ChartTooltip content={<ChartTooltipContent />} />
- <Bar dataKey="泄漏" stackId="a" fill="var(--color-gas)" radius={4} />
- <Bar dataKey="水压异常" stackId="a" fill="var(--color-water)" radius={4} />
- <Bar dataKey="液位过高" stackId="a" fill="var(--color-tank)" radius={4} />
- <Bar dataKey="阀门故障" stackId="a" fill="var(--color-valve)" radius={4} />
- </BarChart>
- </ResponsiveContainer>
- </ChartContainer>
- </CardContent>
- </Card>
- <Card>
- <CardHeader>
- <CardTitle className="text-sm">成因分布(样例)</CardTitle>
- </CardHeader>
- <CardContent>
- <ChartContainer
- config={{
- human: { label: "人为", color: "hsl(var(--chart-5))" },
- aging: { label: "老化", color: "hsl(var(--chart-3))" },
- env: { label: "环境", color: "hsl(var(--chart-2))" },
- }}
- className="h-[240px]"
- >
- <ResponsiveContainer width="100%" height="100%">
- <PieChart>
- <ChartTooltip content={<ChartTooltipContent />} />
- <ChartLegend content={<ChartLegendContent />} />
- <Pie
- data={[
- { name: "人为", value: 36 },
- { name: "老化", value: 44 },
- { name: "环境", value: 20 },
- ]}
- dataKey="value"
- nameKey="name"
- innerRadius={45}
- outerRadius={80}
- paddingAngle={2}
- >
- <Cell key="人为" fill="var(--color-human)" />
- <Cell key="老化" fill="var(--color-aging)" />
- <Cell key="环境" fill="var(--color-env)" />
- </Pie>
- </PieChart>
- </ResponsiveContainer>
- </ChartContainer>
- </CardContent>
- </Card>
- </div>
- </Section>
- <WarningDialog warning={selected} onOpenChange={(open) => !open && setSelected(null)} />
- </div>
- )
- }
- function WarningDialog({
- warning,
- onOpenChange,
- }: {
- warning: Warning | null
- onOpenChange: (open: boolean) => void
- }) {
- return (
- <Dialog open={!!warning} onOpenChange={onOpenChange}>
- <DialogContent className="max-w-2xl">
- <DialogHeader>
- <DialogTitle>预警详情</DialogTitle>
- </DialogHeader>
- {warning ? (
- <div className="text-sm">
- <div className="grid grid-cols-2 gap-3">
- <div>
- <span className="text-muted-foreground">编号:</span>
- {warning.id}
- </div>
- <div>
- <span className="text-muted-foreground">时间:</span>
- {warning.time}
- </div>
- <div>
- <span className="text-muted-foreground">类型:</span>
- {warning.type}
- </div>
- <div>
- <span className="text-muted-foreground">行业:</span>
- {warning.industry}
- </div>
- <div>
- <span className="text-muted-foreground">等级:</span>
- <Badge
- className={
- warning.severity === "高"
- ? "bg-rose-500 hover:bg-rose-500"
- : warning.severity === "中"
- ? "bg-amber-500 hover:bg-amber-500"
- : "bg-emerald-500 hover:bg-emerald-500"
- }
- >
- {warning.severity}
- </Badge>
- </div>
- <div>
- <span className="text-muted-foreground">状态:</span>
- <Badge variant="outline">{warning.status}</Badge>
- </div>
- <div className="col-span-2">
- <span className="text-muted-foreground">区域:</span>
- {warning.district}
- </div>
- </div>
- <Separator className="my-4" />
- <div className="rounded-md border p-3">
- <div className="text-xs text-muted-foreground mb-2">定位(示意)</div>
- <div className="relative h-48 w-full overflow-hidden rounded">
- <svg viewBox="0 0 100 100" className="absolute inset-0 h-full w-full">
- <rect width="100" height="100" fill="hsl(var(--muted))" opacity="0.3" />
- <path d="M 0 50 L 100 50" stroke="hsl(var(--border))" strokeWidth="1" />
- <circle cx="62" cy="42" r="3" fill="hsl(var(--primary))" />
- </svg>
- </div>
- </div>
- </div>
- ) : null}
- </DialogContent>
- </Dialog>
- )
- }
|