map-distribution.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. "use client"
  2. import {Card} from "@/components/ui/card"
  3. import {Badge} from "@/components/ui/badge"
  4. import {useMemo} from "react"
  5. type MapDistributionProps = {
  6. industry?: string
  7. risk?: string
  8. }
  9. type DistrictPoint = {
  10. id: string
  11. x: number // 0..1 relative
  12. y: number // 0..1 relative
  13. risk: "low" | "mid" | "high"
  14. industry: "gas" | "water" | "drain"
  15. }
  16. function seededRandom(seed: number) {
  17. let x = Math.sin(seed) * 10000
  18. return x - Math.floor(x)
  19. }
  20. function genPoints(seed = 42, count = 120): DistrictPoint[] {
  21. const inds: Array<DistrictPoint["industry"]> = ["gas", "water", "drain"]
  22. const risks: Array<DistrictPoint["risk"]> = ["low", "mid", "high"]
  23. return Array.from({ length: count }).map((_, i) => {
  24. const r1 = seededRandom(seed + i * 1.3)
  25. const r2 = seededRandom(seed + i * 2.7)
  26. const r3 = seededRandom(seed + i * 3.9)
  27. return {
  28. id: `pt-${i}`,
  29. x: r1,
  30. y: r2,
  31. risk: r3 > 0.75 ? "high" : r3 > 0.45 ? "mid" : "low",
  32. industry: inds[Math.floor(r1 * inds.length)]!,
  33. }
  34. })
  35. }
  36. export default function MapDistribution({ industry = "all", risk = "all" }: MapDistributionProps) {
  37. const points = useMemo(() => genPoints(7, 150), [])
  38. const filtered = points.filter((p) => (industry === "all" || p.industry === industry) && (risk === "all" || p.risk === risk))
  39. const riskColor = (r: DistrictPoint["risk"]) =>
  40. r === "high" ? "bg-rose-500" : r === "mid" ? "bg-amber-500" : "bg-emerald-500"
  41. return (
  42. <Card className="relative h-[420px] w-full overflow-hidden">
  43. {/* Background grid as simplified "city map" */}
  44. <svg viewBox="0 0 100 100" className="absolute inset-0 h-full w-full">
  45. <defs>
  46. <pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse">
  47. <path d="M 10 0 L 0 0 0 10" fill="none" stroke="hsl(var(--muted-foreground))" opacity="0.2" strokeWidth="0.5" />
  48. </pattern>
  49. </defs>
  50. <rect width="100" height="100" fill="url(#grid)" />
  51. {/* Water bodies / parks (decorative) */}
  52. <rect x="5" y="12" width="30" height="10" rx="2" fill="hsl(var(--muted))" opacity="0.6" />
  53. <rect x="60" y="70" width="30" height="12" rx="2" fill="hsl(var(--muted))" opacity="0.6" />
  54. {/* Main roads */}
  55. <path d="M 0 50 L 100 50" stroke="hsl(var(--border))" strokeWidth="1.5" opacity="0.5" />
  56. <path d="M 25 0 L 75 100" stroke="hsl(var(--border))" strokeWidth="1.5" opacity="0.5" />
  57. </svg>
  58. {/* Points */}
  59. <div className="absolute inset-0">
  60. {filtered.map((p) => (
  61. <div
  62. key={p.id}
  63. className={`absolute h-2.5 w-2.5 rounded-full ring-2 ring-white/70 shadow ${riskColor(p.risk)}`}
  64. style={{
  65. left: `calc(${p.x * 100}% - 5px)`,
  66. top: `calc(${p.y * 100}% - 5px)`,
  67. }}
  68. title={`${p.industry === "gas" ? "燃气" : p.industry === "water" ? "供水" : "排水"} · ${
  69. p.risk === "high" ? "高" : p.risk === "mid" ? "中" : "低"
  70. }风险`}
  71. aria-label="监测点"
  72. />
  73. ))}
  74. </div>
  75. {/* Legend */}
  76. <div className="absolute bottom-3 left-3 flex items-center gap-2 rounded-md bg-background/80 p-2 text-xs shadow">
  77. <span className="inline-flex h-2.5 w-2.5 rounded-full bg-emerald-500" />
  78. <span className="inline-flex h-2.5 w-2.5 rounded-full bg-amber-500 ml-2" />
  79. <span className="inline-flex h-2.5 w-2.5 rounded-full bg-rose-500 ml-2" />
  80. <div className="ml-3 flex items-center gap-1">
  81. <Badge variant="outline">{industry === "all" ? "全部行业" : industry === "gas" ? "燃气" : industry === "water" ? "供水" : "排水"}</Badge>
  82. <Badge variant="outline">{risk === "all" ? "全部风险" : risk === "high" ? "高风险" : risk === "mid" ? "中风险" : "低风险"}</Badge>
  83. </div>
  84. </div>
  85. </Card>
  86. )
  87. }