page.tsx 58 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383
  1. "use client"
  2. import React, {useEffect, useMemo, useState} from "react"
  3. import type {MenuProps, TableColumnsType} from "antd"
  4. import {
  5. Badge,
  6. Button,
  7. Card,
  8. Col,
  9. ConfigProvider,
  10. DatePicker,
  11. Descriptions,
  12. Divider,
  13. Drawer,
  14. Empty,
  15. Layout,
  16. Menu,
  17. Progress,
  18. Row,
  19. Segmented,
  20. Select,
  21. Space,
  22. Statistic,
  23. Switch,
  24. Table,
  25. Tabs,
  26. Tag,
  27. Timeline,
  28. Tooltip as AntdTooltip,
  29. } from "antd"
  30. import {
  31. Activity,
  32. AlertTriangle,
  33. BellRing,
  34. ClipboardList,
  35. Database,
  36. Droplets,
  37. Factory,
  38. Flame,
  39. Gauge,
  40. Layers3,
  41. LineChartIcon,
  42. Moon,
  43. Settings2,
  44. ShieldCheck,
  45. Sun,
  46. Table2,
  47. TrendingUp,
  48. Users,
  49. Waves
  50. } from 'lucide-react'
  51. import EChart from "@/components/echarts"
  52. import dayjs from "dayjs"
  53. import "dayjs/locale/zh-cn"
  54. import zhCN from "antd/es/locale/zh_CN"
  55. import 'leaflet/dist/leaflet.css'
  56. import GisMapBaidu from "@/components/gisMapBaidu";
  57. // 动态导入Leaflet,只在客户端渲染
  58. const L = typeof window !== 'undefined' ? require('leaflet') : null
  59. // 修复Leaflet默认图标问题(仅在客户端)
  60. const fixLeafletIcons = () => {
  61. if (typeof window !== 'undefined' && L) {
  62. delete (L.Icon.Default.prototype as any)._getIconUrl;
  63. L.Icon.Default.mergeOptions({
  64. iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
  65. iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
  66. shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
  67. });
  68. }
  69. };
  70. fixLeafletIcons();
  71. dayjs.locale("zh-cn")
  72. const { Header, Sider, Content } = Layout
  73. const { RangePicker } = DatePicker
  74. // Helpers
  75. const primary = "#10b981" // teal, avoid blue
  76. const danger = "#ef4444"
  77. const warn = "#f59e0b"
  78. const ok = "#22c55e"
  79. const muted = "#6b7280"
  80. type Facility = {
  81. id: string
  82. industry: "燃气" | "供水" | "排水"
  83. type: string
  84. count: number
  85. age: number
  86. region: string
  87. }
  88. type Device = {
  89. id: string
  90. type: string
  91. status: "在线" | "离线"
  92. industry: Facility["industry"]
  93. risk: "高" | "中" | "低"
  94. lat: number
  95. lng: number
  96. }
  97. type Alert = {
  98. id: string
  99. level: "I级" | "II级" | "III级" | "IV级"
  100. industry: Facility["industry"]
  101. type: string
  102. time: string
  103. status: "待处置" | "处置中" | "已闭环"
  104. location: string
  105. desc: string
  106. }
  107. function useMockData() {
  108. const [facilities] = useState<Facility[]>(() => {
  109. const regions = ["中心城区", "东区", "西区", "南区", "北区"]
  110. const types = {
  111. 燃气: ["高压管道", "调压站", "门站", "阀门井"],
  112. 供水: ["原水管", "净水厂", "二次供水", "监测点"],
  113. 排水: ["主干管", "泵站", "检查井", "溢流口"],
  114. } as const
  115. let i = 0
  116. return (["燃气", "供水", "排水"] as Facility["industry"][]).flatMap((ind) =>
  117. types[ind].map((t) => ({
  118. id: `f-${i++}`,
  119. industry: ind,
  120. type: t,
  121. count: Math.floor(Math.random() * 900 + 100),
  122. age: Math.floor(Math.random() * 30 + 1),
  123. region: regions[Math.floor(Math.random() * regions.length)],
  124. }))
  125. )
  126. })
  127. const [devices, setDevices] = useState<Device[]>(() => {
  128. const types = ["压力", "流量", "液位", "阀位", "水质", "气体浓度"]
  129. let i = 0
  130. return Array.from({ length: 250 }).map(() => ({
  131. id: `d-${i++}`,
  132. type: types[Math.floor(Math.random() * types.length)],
  133. status: Math.random() > 0.12 ? "在线" : "离线",
  134. industry: (["燃气", "供水", "排水"] as const)[Math.floor(Math.random() * 3)],
  135. risk: Math.random() > 0.8 ? "高" : Math.random() > 0.5 ? "中" : "低",
  136. lat: Math.random(),
  137. lng: Math.random(),
  138. }))
  139. })
  140. // Simulate device status fluctuation
  141. useEffect(() => {
  142. const t = setInterval(() => {
  143. setDevices((prev) =>
  144. prev.map((d) =>
  145. Math.random() > 0.97 ? { ...d, status: d.status === "在线" ? "离线" : "在线" } : d
  146. )
  147. )
  148. }, 4000)
  149. return () => clearInterval(t)
  150. }, [])
  151. const [alerts, setAlerts] = useState<Alert[]>(() => {
  152. const levels: Alert["level"][] = ["I级", "II级", "III级", "IV级"]
  153. const types = ["泄漏", "压力异常", "流量突变", "停电", "液位过低", "浊度异常"]
  154. let i = 0
  155. return Array.from({ length: 30 }).map(() => ({
  156. id: `a-${i++}`,
  157. level: levels[Math.floor(Math.random() * levels.length)],
  158. industry: (["燃气", "供水", "排水"] as const)[Math.floor(Math.random() * 3)],
  159. type: types[Math.floor(Math.random() * types.length)],
  160. time: dayjs().subtract(Math.floor(Math.random() * 200), "minute").format("YYYY-MM-DD HH:mm"),
  161. status: Math.random() > 0.6 ? "已闭环" : Math.random() > 0.3 ? "处置中" : "待处置",
  162. location: ["中心城区", "东区", "西区", "南区", "北区"][Math.floor(Math.random() * 5)],
  163. desc: "自动监测发现异常,已推送至管理单位核实。",
  164. }))
  165. })
  166. // Simulate new alerts
  167. useEffect(() => {
  168. const t = setInterval(() => {
  169. setAlerts((prev) => {
  170. const n: Alert = {
  171. id: `a-${prev.length + 1}`,
  172. level: Math.random() > 0.85 ? "I级" : Math.random() > 0.6 ? "II级" : Math.random() > 0.3 ? "III级" : "IV级",
  173. industry: (["燃气", "供水", "排水"] as const)[Math.floor(Math.random() * 3)],
  174. type: ["泄漏", "压力异常", "流量突变", "停电", "液位过低", "浊度异常"][Math.floor(Math.random() * 6)],
  175. time: dayjs().format("YYYY-MM-DD HH:mm"),
  176. status: "待处置",
  177. location: ["中心城区", "东区", "西区", "南区", "北区"][Math.floor(Math.random() * 5)],
  178. desc: "前端设备上报新预警,请尽快核查。",
  179. }
  180. return [n, ...prev].slice(0, 60)
  181. })
  182. }, 20000)
  183. return () => clearInterval(t)
  184. }, [])
  185. return { facilities, devices, alerts }
  186. }
  187. function LevelTag({ level }: { level: Alert["level"] }) {
  188. const map = {
  189. "I级": { color: danger },
  190. "II级": { color: warn },
  191. "III级": { color: "#f97316" },
  192. "IV级": { color: ok },
  193. } as const
  194. return <Tag color={map[level].color}>{level}</Tag>
  195. }
  196. function StatusBadge({ s }: { s: Alert["status"] }) {
  197. const status = {
  198. 待处置: "error",
  199. 处置中: "processing",
  200. 已闭环: "success",
  201. } as const
  202. // @ts-expect-error antd types for Badge status
  203. return <Badge status={status[s]} text={s} />
  204. }
  205. export default function Page() {
  206. const [collapsed, setCollapsed] = useState(false)
  207. const { facilities, devices, alerts } = useMockData()
  208. const [selectedMenu, setSelectedMenu] = useState("overview")
  209. const [drawerOpen, setDrawerOpen] = useState(false)
  210. const [selectedAlert, setSelectedAlert] = useState<Alert | null>(null)
  211. const [isDark, setIsDark] = useState(false) // 默认白色主题
  212. const [assetTab, setAssetTab] = useState("archive")
  213. const [monitorTab, setMonitorTab] = useState("summary")
  214. const [alertTab, setAlertTab] = useState("current")
  215. const totals = useMemo(() => {
  216. const sum = { 燃气: 0, 供水: 0, 排水: 0 } as Record<Facility["industry"], number>
  217. facilities.forEach((f) => (sum[f.industry] += f.count))
  218. return sum
  219. }, [facilities])
  220. const onlineRate = useMemo(() => {
  221. const total = devices.length
  222. const online = devices.filter((d) => d.status === "在线").length
  223. return Math.round((online / Math.max(total, 1)) * 100)
  224. }, [devices])
  225. const riskDist = useMemo(() => {
  226. const dist = { 高: 0, 中: 0, 低: 0 } as Record<Device["risk"], number>
  227. devices.forEach((d) => (dist[d.risk] += 1))
  228. return dist
  229. }, [devices])
  230. // Menu
  231. const menuItems: MenuProps["items"] = [
  232. { key: "overview", icon: <Layers3 size={18} />, label: "总体概览" },
  233. { key: "asset", icon: <Database size={18} />, label: "基础设施管理" },
  234. { key: "monitor", icon: <Activity size={18} />, label: "运行监测管理" },
  235. { key: "alert", icon: <BellRing size={18} />, label: "预警处置管理" },
  236. { type: "divider" as const },
  237. { key: "devices", icon: <Gauge size={18} />, label: "设备管理" },
  238. { key: "reports", icon: <ClipboardList size={18} />, label: "数据报表" },
  239. { key: "incidents", icon: <Users size={18} />, label: "事件中心" },
  240. { key: "bigscreen", icon: <LineChartIcon size={18} />, label: "数据大屏(示意)" },
  241. { type: "divider" as const },
  242. { key: "settings", icon: <Settings2 size={18} />, label: "设置" },
  243. ]
  244. // Charts Options
  245. const infraPieOption = useMemo(
  246. () => ({
  247. title: { text: "设施类型占比", left: "center", textStyle: { fontSize: 14 } },
  248. tooltip: { trigger: "item" },
  249. legend: { bottom: 0 },
  250. color: [primary, "#f59e0b", "#6366f1", "#ef4444", "#14b8a6", "#a855f7"],
  251. series: [
  252. {
  253. type: "pie",
  254. radius: ["35%", "60%"],
  255. avoidLabelOverlap: false,
  256. itemStyle: { borderRadius: 6, borderColor: "#fff", borderWidth: 2 },
  257. data: ["燃气", "供水", "排水"].map((ind) => ({
  258. name: ind,
  259. value: facilities
  260. .filter((f) => f.industry === ind)
  261. .reduce((acc, f) => acc + f.count, 0),
  262. })),
  263. },
  264. ],
  265. }),
  266. [facilities]
  267. )
  268. const deviceBarOption = useMemo(() => {
  269. const types = Array.from(new Set(devices.map((d) => d.type)))
  270. const byType = types.map((t) => devices.filter((d) => d.type === t).length)
  271. return {
  272. title: { text: "监测设备类型规模", left: "center", textStyle: { fontSize: 14 } },
  273. tooltip: { trigger: "axis" },
  274. xAxis: { type: "category", data: types, axisLabel: { rotate: 20 } },
  275. yAxis: { type: "value" },
  276. grid: { left: 40, right: 10, bottom: 50, top: 40 },
  277. color: [primary],
  278. series: [{ type: "bar", data: byType, barWidth: "50%", itemStyle: { borderRadius: [4, 4, 0, 0] } }],
  279. } as const
  280. }, [devices])
  281. const alertTrendOption = useMemo(() => {
  282. const days = 14
  283. const labels = Array.from({ length: days }).map((_, i) => dayjs().subtract(days - i - 1, "day").format("MM-DD"))
  284. const series = (["燃气", "供水", "排水"] as const).map((ind, idx) => ({
  285. name: ind,
  286. type: "line",
  287. smooth: true,
  288. data: labels.map(() => Math.floor(Math.random() * (idx === 0 ? 12 : idx === 1 ? 9 : 7)) + (idx === 0 ? 3 : 1)),
  289. }))
  290. return {
  291. title: { text: "预警趋势(近14天)", left: "center", textStyle: { fontSize: 14 } },
  292. tooltip: { trigger: "axis" },
  293. legend: { bottom: 0 },
  294. grid: { left: 40, right: 10, bottom: 40, top: 40 },
  295. xAxis: { type: "category", data: labels },
  296. yAxis: { type: "value" },
  297. color: [primary, "#f59e0b", "#a855f7"],
  298. series,
  299. } as const
  300. }, [])
  301. const onlineGaugeOption = useMemo(
  302. () => ({
  303. title: { text: "设备在线率", left: "center", top: 10, textStyle: { fontSize: 14 } },
  304. series: [
  305. {
  306. type: "gauge",
  307. startAngle: 200,
  308. endAngle: -20,
  309. radius: "90%",
  310. pointer: { show: false },
  311. progress: { show: true, width: 16, itemStyle: { color: onlineRate > 95 ? ok : onlineRate > 85 ? "#84cc16" : warn } },
  312. axisLine: { lineStyle: { width: 16 } },
  313. axisTick: { show: false },
  314. splitLine: { show: false },
  315. axisLabel: { show: false },
  316. detail: { valueAnimation: true, formatter: "{value}%", fontSize: 22, offsetCenter: [0, "10%"] },
  317. data: [{ value: onlineRate }],
  318. },
  319. ],
  320. }),
  321. [onlineRate]
  322. )
  323. const riskBarOption = useMemo(
  324. () => ({
  325. title: { text: "风险等级分布", left: "center", textStyle: { fontSize: 14 } },
  326. tooltip: { trigger: "axis" },
  327. grid: { left: 40, right: 10, top: 40, bottom: 20 },
  328. xAxis: { type: "category", data: ["高", "中", "低"] },
  329. yAxis: { type: "value" },
  330. color: [danger, warn, ok],
  331. series: [{ type: "bar", data: [riskDist["高"], riskDist["中"], riskDist["低"]], barWidth: "50%" }],
  332. }),
  333. [riskDist]
  334. )
  335. const efficiencyPieOption = useMemo(() => {
  336. const closed = alerts.filter((a) => a.status === "已闭环").length
  337. const correct = Math.floor(closed * (0.85 + Math.random() * 0.1)) // 假定人工复核正确率
  338. const wrong = Math.max(closed - correct, 0)
  339. return {
  340. title: { text: "预警正确性(闭环内)", left: "center", textStyle: { fontSize: 14 } },
  341. tooltip: { trigger: "item", formatter: "{b}: {c} ({d}%)" },
  342. legend: { bottom: 0 },
  343. color: [ok, danger],
  344. series: [
  345. {
  346. type: "pie",
  347. radius: ["35%", "60%"],
  348. data: [
  349. { name: "正确预警", value: correct },
  350. { name: "误报", value: wrong },
  351. ],
  352. },
  353. ],
  354. } as const
  355. }, [alerts])
  356. const radarCapacityOption = useMemo(
  357. () => ({
  358. title: { text: "应急保障能力雷达", left: "center", textStyle: { fontSize: 14 } },
  359. tooltip: {},
  360. radar: {
  361. indicator: [
  362. { name: "抢修速度", max: 100 },
  363. { name: "物资充足", max: 100 },
  364. { name: "跨部门联动", max: 100 },
  365. { name: "覆盖广度", max: 100 },
  366. { name: "应急演练", max: 100 },
  367. ],
  368. },
  369. color: [primary],
  370. series: [
  371. {
  372. type: "radar",
  373. data: [{ value: [78, 85, 72, 88, 76], name: "当前" }],
  374. areaStyle: { color: primary + "33" },
  375. },
  376. ],
  377. }),
  378. []
  379. )
  380. // Tables
  381. const facilityColumns: TableColumnsType<Facility> = [
  382. { title: "行业", dataIndex: "industry", key: "industry", width: 90 },
  383. { title: "类型", dataIndex: "type", key: "type" },
  384. { title: "数量", dataIndex: "count", key: "count", width: 100 },
  385. { title: "平均年限", dataIndex: "age", key: "age", width: 120, render: (v) => `${v} 年` },
  386. { title: "分布区域", dataIndex: "region", key: "region", width: 120 },
  387. ]
  388. const alertColumns: TableColumnsType<Alert> = [
  389. { title: "等级", dataIndex: "level", key: "level", width: 90, render: (v) => <LevelTag level={v} /> },
  390. { title: "行业", dataIndex: "industry", key: "industry", width: 90 },
  391. { title: "类型", dataIndex: "type", key: "type", width: 140 },
  392. { title: "时间", dataIndex: "time", key: "time", width: 160 },
  393. { title: "状态", dataIndex: "status", key: "status", width: 120, render: (s) => <StatusBadge s={s} /> },
  394. { title: "位置", dataIndex: "location", key: "location", width: 120 },
  395. {
  396. title: "操作",
  397. key: "action",
  398. render: (_, record) => (
  399. <Space size="small">
  400. <a
  401. onClick={() => {
  402. setSelectedAlert(record)
  403. setDrawerOpen(true)
  404. }}
  405. >
  406. 查看
  407. </a>
  408. <a>派单</a>
  409. <a>联动</a>
  410. </Space>
  411. ),
  412. width: 160,
  413. fixed: "right",
  414. },
  415. ]
  416. const deviceColumns: TableColumnsType<Device> = [
  417. { title: "行业", dataIndex: "industry", key: "industry", width: 90 },
  418. { title: "类型", dataIndex: "type", key: "type", width: 100 },
  419. {
  420. title: "状态",
  421. dataIndex: "status",
  422. key: "status",
  423. width: 100,
  424. render: (s) => <Badge status={s === "在线" ? "success" : "default"} text={s} />,
  425. filters: [
  426. { text: "在线", value: "在线" },
  427. { text: "离线", value: "离线" },
  428. ],
  429. onFilter: (v, r) => r.status === v,
  430. },
  431. {
  432. title: "风险",
  433. dataIndex: "risk",
  434. key: "risk",
  435. width: 100,
  436. render: (r) => <Tag color={r === "高" ? danger : r === "中" ? warn : ok}>{r}</Tag>,
  437. filters: [
  438. { text: "高", value: "高" },
  439. { text: "中", value: "中" },
  440. { text: "低", value: "低" },
  441. ],
  442. onFilter: (v, r) => r.risk === v,
  443. },
  444. { title: "编号", dataIndex: "id", key: "id" },
  445. ]
  446. const alertLevelColor = (lvl: Alert["level"]) =>
  447. lvl === "I级" ? danger : lvl === "II级" ? warn : lvl === "III级" ? "#f97316" : ok
  448. // Derived numbers
  449. const stats = [
  450. {
  451. title: "燃气总规模",
  452. value: totals["燃气"],
  453. icon: <Flame className="text-white" size={18} />,
  454. bg: "bg-emerald-500",
  455. },
  456. {
  457. title: "供水总规模",
  458. value: totals["供水"],
  459. icon: <Droplets className="text-white" size={18} />,
  460. bg: "bg-teal-500",
  461. },
  462. {
  463. title: "排水总规模",
  464. value: totals["排水"],
  465. icon: <Waves className="text-white" size={18} />,
  466. bg: "bg-cyan-500",
  467. },
  468. {
  469. title: "在线设备",
  470. value: devices.filter((d) => d.status === "在线").length,
  471. icon: <Gauge className="text-white" size={18} />,
  472. bg: "bg-lime-500",
  473. },
  474. ]
  475. // Map markers for device distribution
  476. const markers = devices.slice(0, 120).map((d) => ({
  477. id: d.id,
  478. top: `${Math.floor(d.lat * 85) + 5}%`,
  479. left: `${Math.floor(d.lng * 90) + 5}%`,
  480. color: d.risk === "高" ? danger : d.risk === "中" ? warn : ok,
  481. title: `${d.industry}/${d.type}(${d.status})`,
  482. }))
  483. const themeTokens = useMemo(
  484. () => ({
  485. token: {
  486. colorPrimary: primary,
  487. colorInfo: primary,
  488. borderRadius: 8,
  489. fontSize: 13,
  490. },
  491. components: {
  492. Layout: {
  493. headerBg: isDark ? "#0f172a" : "#ffffff",
  494. siderBg: isDark ? "#0b1220" : "#ffffff",
  495. bodyBg: isDark ? "#0b1220" : "#f6f7f9",
  496. },
  497. Menu: {
  498. itemSelectedBg: isDark ? "#0ea5a8" : "#d1fae5",
  499. itemSelectedColor: isDark ? "#fff" : "#0f172a",
  500. itemActiveBg: "#0ea5a822",
  501. },
  502. },
  503. }),
  504. [isDark]
  505. )
  506. return (
  507. <ConfigProvider locale={zhCN} theme={themeTokens}>
  508. <Layout style={{ minHeight: "100vh" }}>
  509. <Sider collapsible collapsed={collapsed} onCollapse={setCollapsed} width={240} theme={isDark ? "dark" : "light"}>
  510. <div className="flex items-center gap-2 px-4 py-3">
  511. <ShieldCheck className="text-emerald-400" size={22} />
  512. {!collapsed && (
  513. <div className={isDark ? "text-emerald-100 font-medium" : "text-emerald-700 font-medium"}>
  514. 城市生命线驾驶舱
  515. </div>
  516. )}
  517. </div>
  518. <Menu
  519. theme={isDark ? "dark" : "light"}
  520. mode="inline"
  521. selectedKeys={[selectedMenu]}
  522. onClick={(e) => setSelectedMenu(e.key)}
  523. items={menuItems}
  524. />
  525. </Sider>
  526. <Layout>
  527. <Header className="flex items-center justify-between px-4">
  528. <div className={`flex items-center gap-3 ${isDark ? "text-white" : "text-slate-800"}`}>
  529. <Factory size={18} />
  530. <span className="font-medium">综合运行态势</span>
  531. <span className={`text-xs hidden md:inline ${isDark ? "text-slate-300" : "text-slate-500"}`}>
  532. 更新时间 {dayjs().format("YYYY-MM-DD HH:mm:ss")}
  533. </span>
  534. </div>
  535. <div className="flex items-center gap-3">
  536. <Select
  537. size="small"
  538. defaultValue="全市"
  539. options={[
  540. { value: "全市", label: "全市" },
  541. { value: "中心城区", label: "中心城区" },
  542. { value: "东区", label: "东区" },
  543. { value: "西区", label: "西区" },
  544. { value: "南区", label: "南区" },
  545. { value: "北区", label: "北区" },
  546. ]}
  547. style={{ width: 120 }}
  548. />
  549. <RangePicker size="small" />
  550. <Button size="small" onClick={() => setIsDark((v) => !v)} icon={isDark ? <Sun size={14} /> : <Moon size={14} />}>
  551. {isDark ? "暗色" : "白色"}
  552. </Button>
  553. </div>
  554. </Header>
  555. <Content className="p-4 md:p-6">
  556. {selectedMenu === "overview" && (
  557. <section className="space-y-4">
  558. <Row gutter={[16, 16]}>
  559. {stats.map((s) => (
  560. <Col xs={24} sm={12} md={12} lg={6} key={s.title}>
  561. <Card>
  562. <div className="flex items-center gap-3">
  563. <div className={`w-9 h-9 rounded-md flex items-center justify-center ${s.bg}`}>{s.icon}</div>
  564. <div className="flex-1">
  565. <div className="text-xs text-slate-500">{s.title}</div>
  566. <div className="text-xl font-semibold">{s.value.toLocaleString()}</div>
  567. </div>
  568. <AntdTooltip title="环比增长">
  569. <TrendingUp className="text-emerald-500" size={18} />
  570. </AntdTooltip>
  571. </div>
  572. </Card>
  573. </Col>
  574. ))}
  575. </Row>
  576. <Row gutter={[16, 16]}>
  577. <Col xs={24} md={12} lg={8}>
  578. <Card>
  579. <EChart option={infraPieOption} />
  580. </Card>
  581. </Col>
  582. <Col xs={24} md={12} lg={8}>
  583. <Card>
  584. <EChart option={deviceBarOption} />
  585. </Card>
  586. </Col>
  587. <Col xs={24} md={24} lg={8}>
  588. <Card>
  589. <EChart option={onlineGaugeOption} />
  590. </Card>
  591. </Col>
  592. </Row>
  593. <Row gutter={[16, 16]}>
  594. <Col xs={24} md={12}>
  595. <Card>
  596. <EChart option={alertTrendOption} />
  597. </Card>
  598. </Col>
  599. <Col xs={24} md={12}>
  600. <Card>
  601. <EChart option={riskBarOption} />
  602. </Card>
  603. </Col>
  604. </Row>
  605. <Card title="监测分布(示意)" extra={<span className="text-xs text-slate-500">标注颜色代表风险等级</span>}>
  606. <GisMapBaidu height={'50'}/>
  607. </Card>
  608. </section>
  609. )}
  610. {selectedMenu === "asset" && (
  611. <section className="space-y-4">
  612. <Tabs
  613. activeKey={assetTab}
  614. onChange={setAssetTab}
  615. items={[
  616. { key: "archive", label: "基础档案" },
  617. { key: "run", label: "运行信息" },
  618. { key: "mgmt", label: "管理信息" },
  619. { key: "emergency", label: "应急保障" },
  620. ]}
  621. />
  622. {assetTab === "archive" && (
  623. <>
  624. <Row gutter={[16, 16]}>
  625. <Col xs={24} lg={14}>
  626. <Card title="设施基础档案">
  627. <Table
  628. size="small"
  629. rowKey="id"
  630. columns={facilityColumns}
  631. dataSource={facilities}
  632. pagination={{ pageSize: 8 }}
  633. scroll={{ x: 700 }}
  634. />
  635. </Card>
  636. </Col>
  637. <Col xs={24} lg={10}>
  638. <Card title="行业重点指标">
  639. <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
  640. <div>
  641. <div className="text-xs text-slate-500 mb-1">平均设施年限</div>
  642. <Statistic value={Math.round(facilities.reduce((a, b) => a + b.age, 0) / facilities.length)} suffix="年" />
  643. <Progress percent={Math.min(100, Math.round((facilities.filter((f) => f.age >= 20).length / facilities.length) * 100))} size="small" />
  644. <div className="text-xs text-slate-500 mt-1">20年以上占比</div>
  645. </div>
  646. <div>
  647. <div className="text-xs text-slate-500 mb-1">重点设施数量</div>
  648. <Statistic value={facilities.filter((f) => f.type.includes("主") || f.type.includes("厂")).reduce((a, b) => a + b.count, 0)} />
  649. <div className="text-xs text-slate-500 mt-1">主干/厂站等关键设施总量</div>
  650. </div>
  651. </div>
  652. </Card>
  653. <Card className="mt-4" title="分布热点(示意)">
  654. <EChart
  655. option={{
  656. title: { text: "区域设施数量", left: "center", textStyle: { fontSize: 14 } },
  657. xAxis: { type: "category", data: ["中心城区", "东区", "西区", "南区", "北区"] },
  658. yAxis: { type: "value" },
  659. grid: { left: 40, right: 10, bottom: 20, top: 40 },
  660. color: [primary],
  661. series: [
  662. {
  663. type: "bar",
  664. data: ["中心城区", "东区", "西区", "南区", "北区"].map(
  665. (r) => facilities.filter((f) => f.region === r).reduce((a, b) => a + b.count, 0)
  666. ),
  667. barWidth: "50%",
  668. },
  669. ],
  670. }}
  671. />
  672. </Card>
  673. </Col>
  674. </Row>
  675. </>
  676. )}
  677. {assetTab === "run" && (
  678. <>
  679. <Row gutter={[16, 16]}>
  680. <Col xs={24} md={12}>
  681. <Card title="日常供应能力(示意)">
  682. <EChart
  683. option={{
  684. tooltip: { trigger: "axis" },
  685. legend: { bottom: 0 },
  686. grid: { left: 40, right: 10, top: 20, bottom: 40 },
  687. xAxis: { type: "category", data: Array.from({ length: 24 }).map((_, i) => `${i}:00`) },
  688. yAxis: { type: "value" },
  689. color: [primary, "#f59e0b"],
  690. series: [
  691. { name: "供水(万m³/h)", type: "line", smooth: true, data: Array.from({ length: 24 }).map(() => 50 + Math.random() * 20) },
  692. { name: "燃气(万m³/h)", type: "line", smooth: true, data: Array.from({ length: 24 }).map(() => 30 + Math.random() * 15) },
  693. ],
  694. }}
  695. />
  696. </Card>
  697. </Col>
  698. <Col xs={24} md={12}>
  699. <Card title="承载能力(示意)">
  700. <EChart
  701. option={{
  702. tooltip: { trigger: "axis" },
  703. grid: { left: 40, right: 10, top: 20, bottom: 40 },
  704. xAxis: { type: "category", data: ["中心", "东", "西", "南", "北"] },
  705. yAxis: { type: "value" },
  706. color: [primary],
  707. series: [{ type: "bar", data: [92, 86, 74, 80, 77] }],
  708. }}
  709. />
  710. </Card>
  711. </Col>
  712. </Row>
  713. </>
  714. )}
  715. {assetTab === "mgmt" && (
  716. <>
  717. <Row gutter={[16, 16]}>
  718. <Col xs={24} md={12}>
  719. <Card title="管理单位信息">
  720. <Descriptions size="small" column={1} bordered>
  721. <Descriptions.Item label="燃气管理单位">市燃气集团、区属燃气公司等(共 6 家)</Descriptions.Item>
  722. <Descriptions.Item label="供水管理单位">市自来水公司、二供物业等(共 9 家)</Descriptions.Item>
  723. <Descriptions.Item label="排水管理单位">城排中心、各区运营公司(共 7 家)</Descriptions.Item>
  724. <Descriptions.Item label="产权分布">市属 60%,区属 30%,社会 10%</Descriptions.Item>
  725. </Descriptions>
  726. <Divider className="my-3" />
  727. <Timeline
  728. items={[
  729. { color: "green", children: "制度更新:设施巡检标准(本月)" },
  730. { color: "blue", children: "完成年度普查 80%" },
  731. { color: "orange", children: "外包维保单位进场(上周)" },
  732. ]}
  733. />
  734. </Card>
  735. </Col>
  736. <Col xs={24} md={12}>
  737. <Card title="产权分布">
  738. <EChart
  739. option={{
  740. tooltip: { trigger: "item" },
  741. legend: { bottom: 0 },
  742. color: [primary, "#34d399", "#a7f3d0"],
  743. series: [
  744. { type: "pie", radius: ["35%", "60%"], data: [{ value: 60, name: "市属" }, { value: 30, name: "区属" }, { value: 10, name: "社会" }] },
  745. ],
  746. }}
  747. />
  748. </Card>
  749. </Col>
  750. </Row>
  751. </>
  752. )}
  753. {assetTab === "emergency" && (
  754. <>
  755. <Row gutter={[16, 16]}>
  756. <Col xs={24} md={12}>
  757. <Card title="应急资源配置">
  758. <div className="grid grid-cols-2 gap-4">
  759. <Card size="small">
  760. <div className="text-xs text-slate-500">物资仓库</div>
  761. <div className="text-xl font-semibold mt-1">12 处</div>
  762. <Progress percent={78} status="active" size="small" />
  763. <div className="text-xs text-slate-500 mt-1">库存充足度</div>
  764. </Card>
  765. <Card size="small">
  766. <div className="text-xs text-slate-500">抢修队伍</div>
  767. <div className="text-xl font-semibold mt-1">26 支</div>
  768. <Progress percent={86} status="active" size="small" />
  769. <div className="text-xs text-slate-500 mt-1">到岗率</div>
  770. </Card>
  771. <Card size="small">
  772. <div className="text-xs text-slate-500">重点防护目标</div>
  773. <div className="text-xl font-semibold mt-1">143 处</div>
  774. <Progress percent={92} status="active" size="small" />
  775. <div className="text-xs text-slate-500 mt-1">纳入台账比例</div>
  776. </Card>
  777. <Card size="small">
  778. <div className="text-xs text-slate-500">联动单位</div>
  779. <div className="text-xl font-semibold mt-1">19 家</div>
  780. <Progress percent={74} status="active" size="small" />
  781. <div className="text-xs text-slate-500 mt-1">联动成熟度</div>
  782. </Card>
  783. </div>
  784. </Card>
  785. </Col>
  786. <Col xs={24} md={12}>
  787. <Card>
  788. <EChart option={radarCapacityOption} />
  789. </Card>
  790. </Col>
  791. </Row>
  792. </>
  793. )}
  794. </section>
  795. )}
  796. {selectedMenu === "monitor" && (
  797. <section className="space-y-4">
  798. <Tabs
  799. activeKey={monitorTab}
  800. onChange={setMonitorTab}
  801. items={[
  802. { key: "summary", label: "监测概览" },
  803. { key: "distribution", label: "监测分布" },
  804. { key: "running", label: "运行概况" },
  805. ]}
  806. />
  807. {monitorTab === "summary" && (
  808. <>
  809. <Row gutter={[16, 16]}>
  810. <Col xs={24} md={12} lg={8}>
  811. <Card>
  812. <EChart option={onlineGaugeOption} />
  813. </Card>
  814. </Col>
  815. <Col xs={24} md={12} lg={8}>
  816. <Card>
  817. <EChart option={deviceBarOption} />
  818. </Card>
  819. </Col>
  820. <Col xs={24} md={24} lg={8}>
  821. <Card>
  822. <EChart option={riskBarOption} />
  823. </Card>
  824. </Col>
  825. </Row>
  826. </>
  827. )}
  828. {monitorTab === "distribution" && (
  829. <>
  830. <Card title="监测分布(按行业与风险)">
  831. <Row gutter={[16, 16]}>
  832. {(["燃气", "供水", "排水"] as const).map((ind) => (
  833. <Col xs={24} md={8} key={ind}>
  834. <Card size="small" title={ind}>
  835. <div className="flex items-center gap-4">
  836. <div className="flex-1">
  837. <div className="text-xs text-slate-500">设备在线</div>
  838. <div className="text-lg font-semibold">
  839. {devices.filter((d) => d.industry === ind && d.status === "在线").length}
  840. </div>
  841. <div className="text-xs text-slate-500">总量 {devices.filter((d) => d.industry === ind).length}</div>
  842. </div>
  843. <div className="flex-1">
  844. <div className="text-xs text-slate-500">高风险</div>
  845. <div className="text-lg font-semibold">
  846. {devices.filter((d) => d.industry === ind && d.risk === "高").length}
  847. </div>
  848. <Progress
  849. percent={Math.round(
  850. (devices.filter((d) => d.industry === ind && d.risk === "高").length /
  851. Math.max(devices.filter((d) => d.industry === ind).length, 1)) *
  852. 100
  853. )}
  854. size="small"
  855. />
  856. </div>
  857. </div>
  858. </Card>
  859. </Col>
  860. ))}
  861. </Row>
  862. </Card>
  863. <Card title="(示意)">
  864. <div className="relative w-full h-[380px] rounded-md overflow-hidden bg-slate-100">
  865. <img
  866. src="/images/city-map.png"
  867. alt="城市简化地图"
  868. className="absolute inset-0 w-full h-full object-cover opacity-90"
  869. />
  870. {markers.map((m) => (
  871. <AntdTooltip key={m.id} title={m.title}>
  872. <div
  873. className="absolute w-2.5 h-2.5 rounded-full ring-2 ring-white/70"
  874. style={{ top: m.top, left: m.left, backgroundColor: m.color }}
  875. />
  876. </AntdTooltip>
  877. ))}
  878. </div>
  879. </Card>
  880. </>
  881. )}
  882. {monitorTab === "running" && (
  883. <>
  884. <Card title="运行概况(报警覆盖)">
  885. <EChart
  886. option={{
  887. tooltip: { trigger: "axis" },
  888. legend: { bottom: 0 },
  889. grid: { left: 40, right: 10, top: 20, bottom: 40 },
  890. xAxis: { type: "category", data: Array.from({ length: 12 }).map((_, i) => dayjs().subtract(11 - i, "month").format("YYYY-MM")) },
  891. yAxis: { type: "value" },
  892. color: [primary, "#f59e0b", "#a855f7"],
  893. series: (["燃气", "供水", "排水"] as const).map((ind, idx) => ({
  894. name: ind,
  895. type: "bar",
  896. stack: "sum",
  897. emphasis: { focus: "series" },
  898. data: Array.from({ length: 12 }).map(() => Math.floor(Math.random() * (idx === 0 ? 50 : idx === 1 ? 40 : 30))),
  899. })),
  900. }}
  901. />
  902. </Card>
  903. <Row gutter={[16, 16]} className="mt-1">
  904. <Col xs={24} md={12}>
  905. <Card>
  906. <EChart option={alertTrendOption} />
  907. </Card>
  908. </Col>
  909. <Col xs={24} md={12}>
  910. <Card>
  911. <EChart option={deviceBarOption} />
  912. </Card>
  913. </Col>
  914. </Row>
  915. </>
  916. )}
  917. </section>
  918. )}
  919. {selectedMenu === "alert" && (
  920. <section className="space-y-4">
  921. <Tabs
  922. activeKey={alertTab}
  923. onChange={setAlertTab}
  924. items={[
  925. { key: "current", label: "当前预警" },
  926. { key: "history", label: "历史分析" },
  927. { key: "efficiency", label: "预警效率" },
  928. { key: "analysis", label: "预警分析" },
  929. ]}
  930. />
  931. {alertTab === "current" && (
  932. <>
  933. <Row gutter={[16, 16]}>
  934. <Col xs={24} lg={16}>
  935. <Card title="总体预警处置情况">
  936. <Table
  937. size="small"
  938. rowKey="id"
  939. columns={alertColumns}
  940. dataSource={alerts}
  941. pagination={{ pageSize: 8 }}
  942. onRow={(record) => ({
  943. onClick: () => {
  944. setSelectedAlert(record)
  945. setDrawerOpen(true)
  946. },
  947. })}
  948. scroll={{ x: 900 }}
  949. />
  950. </Card>
  951. </Col>
  952. <Col xs={24} lg={8}>
  953. <Card title="处置效率">
  954. <div className="grid grid-cols-2 gap-4">
  955. <div>
  956. <div className="text-xs text-slate-500">平均处置时效</div>
  957. <div className="text-xl font-semibold mt-1">{(45 + Math.random() * 30).toFixed(0)} 分钟</div>
  958. <Progress percent={78} status="active" size="small" />
  959. <div className="text-xs text-slate-500 mt-1">较上月 +6%</div>
  960. </div>
  961. <div>
  962. <div className="text-xs text-slate-500">闭环率</div>
  963. <div className="text-xl font-semibold mt-1">
  964. {Math.round((alerts.filter((a) => a.status === "已闭环").length / Math.max(alerts.length, 1)) * 100)}%
  965. </div>
  966. <Progress percent={86} status="active" size="small" />
  967. <div className="text-xs text-slate-500 mt-1">较上周 +2%</div>
  968. </div>
  969. </div>
  970. </Card>
  971. <Card className="mt-4">
  972. <EChart option={efficiencyPieOption} />
  973. </Card>
  974. </Col>
  975. </Row>
  976. <Drawer
  977. title={
  978. <div className="flex items-center gap-2">
  979. <AlertTriangle size={18} color={selectedAlert ? alertLevelColor(selectedAlert.level) : primary} />
  980. <span>预警详情</span>
  981. </div>
  982. }
  983. placement="right"
  984. width={520}
  985. open={drawerOpen}
  986. onClose={() => setDrawerOpen(false)}
  987. >
  988. {selectedAlert ? (
  989. <div className="space-y-4">
  990. <Descriptions size="small" column={1} bordered>
  991. <Descriptions.Item label="预警编号">{selectedAlert.id}</Descriptions.Item>
  992. <Descriptions.Item label="等级">
  993. <LevelTag level={selectedAlert.level} />
  994. </Descriptions.Item>
  995. <Descriptions.Item label="行业">{selectedAlert.industry}</Descriptions.Item>
  996. <Descriptions.Item label="类型">{selectedAlert.type}</Descriptions.Item>
  997. <Descriptions.Item label="时间">{selectedAlert.time}</Descriptions.Item>
  998. <Descriptions.Item label="位置">{selectedAlert.location}</Descriptions.Item>
  999. <Descriptions.Item label="状态">
  1000. <StatusBadge s={selectedAlert.status} />
  1001. </Descriptions.Item>
  1002. <Descriptions.Item label="描述">{selectedAlert.desc}</Descriptions.Item>
  1003. </Descriptions>
  1004. <Card size="small" title="事件时序">
  1005. <EChart
  1006. option={{
  1007. grid: { left: 30, right: 10, top: 20, bottom: 20 },
  1008. xAxis: { type: "category", data: ["发现", "确认", "处置", "复盘"] },
  1009. yAxis: { type: "value" },
  1010. color: [primary],
  1011. series: [{ type: "line", smooth: true, data: [0, 12, 38, 60] }],
  1012. }}
  1013. />
  1014. </Card>
  1015. <Card size="small" title="一张图联动(示意)">
  1016. <div className="relative w-full h-[200px] rounded-md overflow-hidden">
  1017. <img src="/images/city-map.png" alt="地图联动示意图" className="absolute inset-0 w-full h-full object-cover opacity-90" />
  1018. <div className="absolute inset-0">
  1019. <div
  1020. className="absolute w-3 h-3 rounded-full ring-2 ring-white animate-pulse"
  1021. style={{ top: "42%", left: "56%", background: alertLevelColor(selectedAlert.level) }}
  1022. title="预警位置"
  1023. />
  1024. </div>
  1025. </div>
  1026. </Card>
  1027. <Space>
  1028. <a className="text-emerald-600">查看工单</a>
  1029. <a className="text-emerald-600">下达指令</a>
  1030. <a className="text-emerald-600">通知联动</a>
  1031. </Space>
  1032. </div>
  1033. ) : (
  1034. <div className="text-slate-500 text-sm">请选择左侧预警查看详情。</div>
  1035. )}
  1036. </Drawer>
  1037. </>
  1038. )}
  1039. {alertTab === "history" && (
  1040. <>
  1041. <Row gutter={[16, 16]}>
  1042. <Col xs={24} md={12}>
  1043. <Card title="各行业预警数量(近6月)">
  1044. <EChart
  1045. option={{
  1046. tooltip: { trigger: "axis" },
  1047. legend: { bottom: 0 },
  1048. grid: { left: 40, right: 10, top: 20, bottom: 40 },
  1049. xAxis: { type: "category", data: Array.from({ length: 6 }).map((_, i) => dayjs().subtract(5 - i, "month").format("YYYY-MM")) },
  1050. yAxis: { type: "value" },
  1051. color: [primary, "#f59e0b", "#a855f7"],
  1052. series: (["燃气", "供水", "排水"] as const).map((ind, idx) => ({
  1053. name: ind,
  1054. type: "line",
  1055. smooth: true,
  1056. data: Array.from({ length: 6 }).map(() => Math.floor(Math.random() * (idx === 0 ? 60 : idx === 1 ? 45 : 35)) + 5),
  1057. })),
  1058. }}
  1059. />
  1060. </Card>
  1061. </Col>
  1062. <Col xs={24} md={12}>
  1063. <Card title="已闭环预警(示意)">
  1064. <Table
  1065. size="small"
  1066. rowKey="id"
  1067. columns={[
  1068. { title: "编号", dataIndex: "id" },
  1069. { title: "类型", dataIndex: "type" },
  1070. { title: "行业", dataIndex: "industry", width: 90 },
  1071. { title: "完成时间", dataIndex: "time", width: 160 },
  1072. ]}
  1073. dataSource={alerts.filter((a) => a.status === "已闭环")}
  1074. pagination={{ pageSize: 6 }}
  1075. />
  1076. </Card>
  1077. </Col>
  1078. </Row>
  1079. </>
  1080. )}
  1081. {alertTab === "efficiency" && (
  1082. <>
  1083. <Row gutter={[16, 16]}>
  1084. <Col xs={24} md={12}>
  1085. <Card title="整体效率">
  1086. <div className="grid grid-cols-2 gap-4">
  1087. <div>
  1088. <div className="text-xs text-slate-500">平均处置时效</div>
  1089. <div className="text-xl font-semibold mt-1">{(42 + Math.random() * 25).toFixed(0)} 分钟</div>
  1090. <Progress percent={82} status="active" size="small" />
  1091. <div className="text-xs text-slate-500 mt-1">较上月 +4%</div>
  1092. </div>
  1093. <div>
  1094. <div className="text-xs text-slate-500">正确预警率</div>
  1095. <div className="text-xl font-semibold mt-1">{(88 + Math.random() * 4).toFixed(1)}%</div>
  1096. <Progress percent={89} status="active" size="small" />
  1097. <div className="text-xs text-slate-500 mt-1">核实误报下降</div>
  1098. </div>
  1099. </div>
  1100. </Card>
  1101. </Col>
  1102. <Col xs={24} md={12}>
  1103. <Card>
  1104. <EChart option={efficiencyPieOption} />
  1105. </Card>
  1106. </Col>
  1107. </Row>
  1108. </>
  1109. )}
  1110. {alertTab === "analysis" && (
  1111. <>
  1112. <Row gutter={[16, 16]}>
  1113. <Col xs={24}>
  1114. <Card title="预警类型与成因分布">
  1115. <EChart
  1116. option={{
  1117. tooltip: { trigger: "axis" },
  1118. legend: { bottom: 0 },
  1119. grid: { left: 40, right: 10, top: 20, bottom: 40 },
  1120. xAxis: {
  1121. type: "category",
  1122. data: ["泄漏", "压力异常", "流量突变", "停电", "液位过低", "浊度异常"],
  1123. axisLabel: { rotate: 20 },
  1124. },
  1125. yAxis: { type: "value" },
  1126. color: [primary, "#f59e0b"],
  1127. series: [
  1128. { name: "设备原因", type: "bar", data: [30, 22, 18, 12, 9, 7] },
  1129. { name: "外部原因", type: "bar", data: [18, 15, 14, 28, 11, 10] },
  1130. ],
  1131. }}
  1132. />
  1133. </Card>
  1134. </Col>
  1135. </Row>
  1136. </>
  1137. )}
  1138. </section>
  1139. )}
  1140. {selectedMenu === "devices" && (
  1141. <section className="space-y-4">
  1142. <Row justify="space-between" align="middle">
  1143. <Col>
  1144. <div className="text-base font-medium">设备管理</div>
  1145. </Col>
  1146. <Col>
  1147. <Space>
  1148. <Segmented
  1149. options={["全部", "在线", "离线"]}
  1150. onChange={() => {}}
  1151. aria-label="设备状态筛选"
  1152. />
  1153. <Button size="small" icon={<Table2 size={14} />}>
  1154. 批量导出
  1155. </Button>
  1156. </Space>
  1157. </Col>
  1158. </Row>
  1159. <Card>
  1160. <Table
  1161. size="small"
  1162. rowKey="id"
  1163. columns={deviceColumns}
  1164. dataSource={devices}
  1165. pagination={{ pageSize: 10 }}
  1166. scroll={{ x: 700 }}
  1167. />
  1168. </Card>
  1169. </section>
  1170. )}
  1171. {selectedMenu === "reports" && (
  1172. <section className="space-y-4">
  1173. <Row gutter={[16, 16]}>
  1174. <Col xs={24} md={12}>
  1175. <Card title="月度运行报告(示意)" extra={<Button size="small">导出 PDF</Button>}>
  1176. <EChart
  1177. option={{
  1178. tooltip: { trigger: "axis" },
  1179. grid: { left: 40, right: 10, top: 20, bottom: 30 },
  1180. xAxis: { type: "category", data: ["一", "二", "三", "四", "五", "六"] },
  1181. yAxis: { type: "value" },
  1182. color: [primary],
  1183. series: [{ type: "line", smooth: true, data: [120, 132, 101, 134, 90, 230] }],
  1184. }}
  1185. />
  1186. </Card>
  1187. </Col>
  1188. <Col xs={24} md={12}>
  1189. <Card title="安全事件统计(示意)" extra={<Button size="small">导出 Excel</Button>}>
  1190. <EChart
  1191. option={{
  1192. tooltip: { trigger: "axis" },
  1193. grid: { left: 40, right: 10, top: 20, bottom: 30 },
  1194. xAxis: { type: "category", data: ["燃气", "供水", "排水"] },
  1195. yAxis: { type: "value" },
  1196. color: [primary, "#f59e0b", "#a855f7"],
  1197. series: [{ type: "bar", data: [32, 21, 17], barWidth: "50%" }],
  1198. }}
  1199. />
  1200. </Card>
  1201. </Col>
  1202. </Row>
  1203. </section>
  1204. )}
  1205. {selectedMenu === "incidents" && (
  1206. <section className="space-y-4">
  1207. <Row gutter={[16, 16]}>
  1208. <Col xs={24} md={16}>
  1209. <Card title="事件列表(示意)" extra={<Button size="small">新建事件</Button>}>
  1210. <Empty description="暂无数据(示意)" />
  1211. </Card>
  1212. </Col>
  1213. <Col xs={24} md={8}>
  1214. <Card title="事件趋势(示意)">
  1215. <EChart
  1216. option={{
  1217. tooltip: { trigger: "axis" },
  1218. grid: { left: 40, right: 10, top: 20, bottom: 30 },
  1219. xAxis: { type: "category", data: Array.from({ length: 12 }).map((_, i) => `${i + 1}月`) },
  1220. yAxis: { type: "value" },
  1221. color: [primary],
  1222. series: [{ type: "line", smooth: true, data: Array.from({ length: 12 }).map(() => Math.floor(Math.random() * 20) + 5) }],
  1223. }}
  1224. />
  1225. </Card>
  1226. </Col>
  1227. </Row>
  1228. </section>
  1229. )}
  1230. {selectedMenu === "bigscreen" && (
  1231. <section className="space-y-4">
  1232. <Card title="数据大屏(示意)">
  1233. <Row gutter={[16, 16]}>
  1234. {[
  1235. { title: "设备总数", val: devices.length },
  1236. { title: "在线率", val: `${onlineRate}%` },
  1237. { title: "本月预警", val: (alerts.length + Math.floor(Math.random() * 30)).toString() },
  1238. { title: "闭环率", val: `${Math.round((alerts.filter((a) => a.status === "已闭环").length / Math.max(alerts.length, 1)) * 100)}%` },
  1239. ].map((s) => (
  1240. <Col xs={12} md={6} key={s.title}>
  1241. <Card size="small">
  1242. <div className="text-xs text-slate-500">{s.title}</div>
  1243. <div className="text-2xl font-semibold mt-1">{s.val}</div>
  1244. </Card>
  1245. </Col>
  1246. ))}
  1247. </Row>
  1248. <Row gutter={[16, 16]} className="mt-1">
  1249. <Col xs={24} md={12}>
  1250. <Card>
  1251. <EChart option={deviceBarOption} />
  1252. </Card>
  1253. </Col>
  1254. <Col xs={24} md={12}>
  1255. <Card>
  1256. <EChart option={riskBarOption} />
  1257. </Card>
  1258. </Col>
  1259. </Row>
  1260. </Card>
  1261. </section>
  1262. )}
  1263. {selectedMenu === "settings" && (
  1264. <section className="space-y-4">
  1265. <Card title="预警设置">
  1266. <Row gutter={[16, 16]}>
  1267. <Col xs={24} md={12}>
  1268. <Card size="small" title="阈值设置">
  1269. <div className="grid grid-cols-2 gap-4">
  1270. <div>
  1271. <div className="text-xs text-slate-500 mb-1">压力异常阈值</div>
  1272. <Select defaultValue="P≤0.3MPa" options={[{ value: "P≤0.3MPa", label: "P≤0.3MPa" }, { value: "P≤0.2MPa", label: "P≤0.2MPa" }]} />
  1273. </div>
  1274. <div>
  1275. <div className="text-xs text-slate-500 mb-1">流量突变阈值</div>
  1276. <Select defaultValue="ΔQ≥20%" options={[{ value: "ΔQ≥20%", label: "ΔQ≥20%" }, { value: "ΔQ≥30%", label: "ΔQ≥30%" }]} />
  1277. </div>
  1278. <div>
  1279. <div className="text-xs text-slate-500 mb-1">水质指标(浊度)</div>
  1280. <Select defaultValue="≥5 NTU" options={[{ value: "≥5 NTU", label: "≥5 NTU" }, { value: "≥3 NTU", label: "≥3 NTU" }]} />
  1281. </div>
  1282. <div>
  1283. <div className="text-xs text-slate-500 mb-1">液位过低</div>
  1284. <Select defaultValue="≤20%" options={[{ value: "≤20%", label: "≤20%" }, { value: "≤15%", label: "≤15%" }]} />
  1285. </div>
  1286. </div>
  1287. </Card>
  1288. </Col>
  1289. <Col xs={24} md={12}>
  1290. <Card size="small" title="通知策略">
  1291. <div className="grid grid-cols-2 gap-4">
  1292. <div className="flex items-center justify-between">
  1293. <span>高等级预警短信</span>
  1294. <Switch defaultChecked />
  1295. </div>
  1296. <div className="flex items-center justify-between">
  1297. <span>邮件抄送管理层</span>
  1298. <Switch />
  1299. </div>
  1300. <div className="flex items-center justify-between">
  1301. <span>自动派单</span>
  1302. <Switch />
  1303. </div>
  1304. <div className="flex items-center justify-between">
  1305. <span>与值守大屏联动</span>
  1306. <Switch defaultChecked />
  1307. </div>
  1308. </div>
  1309. </Card>
  1310. </Col>
  1311. </Row>
  1312. </Card>
  1313. </section>
  1314. )}
  1315. </Content>
  1316. </Layout>
  1317. </Layout>
  1318. </ConfigProvider>
  1319. )
  1320. }