page.tsx 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225
  1. "use client"
  2. import {useCallback, useEffect, useState} from "react"
  3. import dynamic from "next/dynamic"
  4. import {
  5. Badge,
  6. Button,
  7. Card,
  8. Checkbox,
  9. Col,
  10. DatePicker,
  11. Dropdown,
  12. Form,
  13. Input,
  14. Layout,
  15. Menu,
  16. Modal,
  17. Progress,
  18. Row,
  19. Select,
  20. Space,
  21. Statistic,
  22. Switch,
  23. Table,
  24. Tabs,
  25. Tag,
  26. } from "antd"
  27. // 为避免未定义错误,添加缺失的图标导入
  28. import {
  29. BarChartOutlined,
  30. BellOutlined,
  31. BlockOutlined,
  32. CheckOutlined,
  33. DashboardOutlined,
  34. DownOutlined,
  35. EnvironmentOutlined,
  36. ExportOutlined,
  37. FilterOutlined,
  38. LineChartOutlined,
  39. MonitorOutlined,
  40. PieChartOutlined,
  41. ReloadOutlined,
  42. SearchOutlined,
  43. SettingOutlined,
  44. UserOutlined,
  45. VideoCameraOutlined,
  46. WarningOutlined,
  47. } from "@ant-design/icons"
  48. import MonitoringCharts from "./components/MonitoringCharts"
  49. import AlarmPanel from "./components/AlarmPanel"
  50. import VideoMonitoring from "./components/VideoMonitoring"
  51. import DataManagement from "./components/DataManagement"
  52. import globalMessage from "@/app/_modules/globalMessage"
  53. import GisMapBaidu from "@/components/gisMapBaidu";
  54. const { RangePicker } = DatePicker
  55. const MapView = dynamic(() => import("./components/MapView"), {
  56. ssr: false,
  57. loading: () => <div className="flex items-center justify-center h-full">地图加载中...</div>,
  58. })
  59. const { Header, Sider, Content } = Layout
  60. const { Option } = Select
  61. const { TabPane } = Tabs
  62. export default function DrainageMonitoringSystem() {
  63. const [selectedMenu, setSelectedMenu] = useState("dashboard")
  64. const [collapsed, setCollapsed] = useState(false)
  65. const [videoModalVisible, setVideoModalVisible] = useState(false)
  66. const [currentTime, setCurrentTime] = useState("")
  67. const [notifications, setNotifications] = useState([
  68. { id: 1, message: "人民路积水点液位超过阈值", time: "2分钟前", type: "warning" },
  69. { id: 2, message: "第一泵站设备离线", time: "15分钟前", type: "error" },
  70. ])
  71. const [autoRefresh, setAutoRefresh] = useState(true)
  72. const [videoSettingsModal, setVideoSettingsModal] = useState(false)
  73. // 只在客户端更新时间,避免服务端和客户端渲染不一致
  74. useEffect(() => {
  75. const updateTime = () => {
  76. const now = new Date()
  77. const hours = now.getHours().toString().padStart(2, "0")
  78. const minutes = now.getMinutes().toString().padStart(2, "0")
  79. const seconds = now.getSeconds().toString().padStart(2, "0")
  80. setCurrentTime(`${hours}:${minutes}:${seconds}`)
  81. }
  82. updateTime()
  83. const interval = setInterval(updateTime, 1000)
  84. return () => clearInterval(interval)
  85. }, [])
  86. // 自动刷新数据
  87. useEffect(() => {
  88. let interval: NodeJS.Timeout | null = null
  89. if (autoRefresh) {
  90. interval = setInterval(() => {
  91. // 这里可以添加刷新数据的逻辑
  92. console.log("自动刷新数据")
  93. }, 30000) // 30秒刷新一次
  94. }
  95. return () => {
  96. if (interval) clearInterval(interval)
  97. }
  98. }, [autoRefresh])
  99. const menuItems = [
  100. {
  101. key: "dashboard",
  102. icon: <DashboardOutlined />,
  103. label: "综合监控",
  104. },
  105. {
  106. key: "data-management",
  107. icon: <EnvironmentOutlined />,
  108. label: "基础数据管理",
  109. },
  110. {
  111. key: "real-time-monitoring",
  112. icon: <MonitorOutlined />,
  113. label: "实时监测",
  114. },
  115. {
  116. key: "alarm-warning",
  117. icon: <WarningOutlined />,
  118. label: "监测报警",
  119. },
  120. {
  121. key: "video-monitoring",
  122. icon: <VideoCameraOutlined />,
  123. label: "视频监测",
  124. },
  125. ]
  126. const handleButtonClick = (action: string) => {
  127. globalMessage.success(`${action} 操作已执行`)
  128. }
  129. const renderContent = () => {
  130. switch (selectedMenu) {
  131. case "dashboard":
  132. return <DashboardContent onButtonClick={handleButtonClick} autoRefresh={autoRefresh} setAutoRefresh={setAutoRefresh} />
  133. case "data-management":
  134. return <DataManagement onButtonClick={handleButtonClick} />
  135. case "real-time-monitoring":
  136. return <RealTimeMonitoring onButtonClick={handleButtonClick} />
  137. case "alarm-warning":
  138. return <AlarmPanel onButtonClick={handleButtonClick} />
  139. case "video-monitoring":
  140. return (
  141. <VideoMonitoringContent
  142. onVideoClick={() => setVideoModalVisible(true)}
  143. onButtonClick={handleButtonClick}
  144. onRefreshVideos={() => console.log("刷新视频列表")}
  145. onVideoSettings={() => setVideoSettingsModal(true)}
  146. />
  147. )
  148. default:
  149. return <DashboardContent onButtonClick={handleButtonClick} autoRefresh={autoRefresh} setAutoRefresh={setAutoRefresh} />
  150. }
  151. }
  152. const notificationMenu = {
  153. items: [
  154. {
  155. key: 'notification-header',
  156. label: (
  157. <div className="flex justify-between items-center">
  158. <h4 className="font-bold">通知</h4>
  159. <Button type="link" size="small">清除所有</Button>
  160. </div>
  161. ),
  162. },
  163. ...notifications.map(notification => ({
  164. key: `notification-${notification.id}`,
  165. label: (
  166. <div className="p-2 hover:bg-gray-50">
  167. <div className="flex justify-between">
  168. <span className={notification.type === "warning" ? "text-orange-500" : "text-red-500"}>
  169. {notification.message}
  170. </span>
  171. <span className="text-gray-400 text-sm">{notification.time}</span>
  172. </div>
  173. </div>
  174. ),
  175. })),
  176. {
  177. key: 'notification-footer',
  178. label: (
  179. <div className="text-center">
  180. <Button type="link">查看全部通知</Button>
  181. </div>
  182. ),
  183. }
  184. ]
  185. }
  186. return (
  187. <Layout className="h-screen">
  188. <Header className="bg-gray-50 border-b border-gray-200 px-4 flex items-center justify-between" style={{ background: "#f8f9fa" }}>
  189. <div className="flex items-center space-x-4">
  190. <h1 className="text-xl font-bold m-0 text-gray-800">排水管网安全运行监测子系统</h1>
  191. </div>
  192. <div className="flex items-center space-x-4 text-gray-600">
  193. <Dropdown menu={notificationMenu} trigger={["click"]}>
  194. <Badge count={notifications.length} size="small">
  195. <BellOutlined className="text-lg cursor-pointer" />
  196. </Badge>
  197. </Dropdown>
  198. <span>管理员</span>
  199. {/* 使用 suppressHydrationWarning 忽略时间显示的 hydration 警告 */}
  200. <span suppressHydrationWarning>{currentTime}</span>
  201. <UserOutlined className="text-lg" />
  202. </div>
  203. </Header>
  204. <Layout className="flex-1">
  205. <Sider
  206. collapsible
  207. collapsed={collapsed}
  208. onCollapse={setCollapsed}
  209. className="bg-gray-50 border-r border-gray-200 h-full"
  210. width={200}
  211. >
  212. <Menu
  213. mode="inline"
  214. selectedKeys={[selectedMenu]}
  215. onClick={({ key }) => setSelectedMenu(key)}
  216. items={menuItems}
  217. className="border-r-0 h-full bg-gray-50"
  218. />
  219. </Sider>
  220. <Content className="p-6 bg-white overflow-auto">{renderContent()}</Content>
  221. </Layout>
  222. <Modal
  223. title="视频监控"
  224. open={videoModalVisible}
  225. onCancel={() => setVideoModalVisible(false)}
  226. width={1200}
  227. footer={[
  228. <Button key="close" onClick={() => setVideoModalVisible(false)}>
  229. 关闭
  230. </Button>,
  231. ]}
  232. >
  233. <VideoMonitoring onButtonClick={handleButtonClick} />
  234. </Modal>
  235. <Modal
  236. title="视频设置"
  237. open={videoSettingsModal}
  238. onCancel={() => setVideoSettingsModal(false)}
  239. width={600}
  240. footer={[
  241. <Button key="cancel" onClick={() => setVideoSettingsModal(false)}>
  242. 取消
  243. </Button>,
  244. <Button key="submit" type="primary" onClick={() => {
  245. setVideoSettingsModal(false)
  246. globalMessage.success("视频设置已保存")
  247. }}>
  248. 保存
  249. </Button>
  250. ]}
  251. >
  252. <VideoSettingsForm />
  253. </Modal>
  254. </Layout>
  255. )
  256. }
  257. function VideoSettingsForm() {
  258. const [form] = Form.useForm()
  259. const onFinish = (values: any) => {
  260. console.log('视频设置:', values)
  261. }
  262. return (
  263. <Form
  264. form={form}
  265. layout="vertical"
  266. onFinish={onFinish}
  267. initialValues={{
  268. quality: 'high',
  269. autoRecord: false,
  270. motionDetection: true,
  271. nightVision: true,
  272. resolution: '1080p',
  273. }}
  274. >
  275. <Form.Item name="quality" label="视频质量">
  276. <Select>
  277. <Option value="low">低</Option>
  278. <Option value="medium">中</Option>
  279. <Option value="high">高</Option>
  280. <Option value="ultra">超高清</Option>
  281. </Select>
  282. </Form.Item>
  283. <Form.Item name="resolution" label="分辨率">
  284. <Select>
  285. <Option value="720p">720p</Option>
  286. <Option value="1080p">1080p</Option>
  287. <Option value="2k">2K</Option>
  288. <Option value="4k">4K</Option>
  289. </Select>
  290. </Form.Item>
  291. <Form.Item name="autoRecord" valuePropName="checked">
  292. <Checkbox>自动录像</Checkbox>
  293. </Form.Item>
  294. <Form.Item name="motionDetection" valuePropName="checked">
  295. <Checkbox>移动侦测</Checkbox>
  296. </Form.Item>
  297. <Form.Item name="nightVision" valuePropName="checked">
  298. <Checkbox>夜视功能</Checkbox>
  299. </Form.Item>
  300. <Form.Item name="storagePath" label="录像存储路径">
  301. <Input placeholder="请输入存储路径" />
  302. </Form.Item>
  303. </Form>
  304. )
  305. }
  306. function DashboardContent({ onButtonClick, autoRefresh, setAutoRefresh }: {
  307. onButtonClick: (action: string) => void,
  308. autoRefresh: boolean,
  309. setAutoRefresh: (value: boolean) => void
  310. }) {
  311. const [deviceStats, setDeviceStats] = useState({
  312. total: 1234,
  313. online: 1180,
  314. alarms: 23,
  315. waterPoints: 156
  316. })
  317. const [timeRange, setTimeRange] = useState<[string, string]>(['今天', ''])
  318. // 模拟数据更新
  319. const updateStats = useCallback(() => {
  320. setDeviceStats(prev => ({
  321. total: prev.total,
  322. online: prev.online + Math.floor(Math.random() * 3) - 1,
  323. alarms: Math.max(0, prev.alarms + Math.floor(Math.random() * 3) - 1),
  324. waterPoints: prev.waterPoints
  325. }))
  326. }, [])
  327. useEffect(() => {
  328. if (autoRefresh) {
  329. const interval = setInterval(updateStats, 10000)
  330. return () => clearInterval(interval)
  331. }
  332. }, [autoRefresh, updateStats])
  333. const timeRangeOptions = [
  334. { label: '今天', value: 'today' },
  335. { label: '昨天', value: 'yesterday' },
  336. { label: '最近7天', value: '7days' },
  337. { label: '最近30天', value: '30days' },
  338. ]
  339. const handleTimeRangeChange = (value: string) => {
  340. setTimeRange([value, ''])
  341. onButtonClick(`切换时间范围到${value}`)
  342. }
  343. return (
  344. <div className="space-y-6">
  345. {/* 控制面板 */
  346. }
  347. <Card size="small">
  348. <div className="flex flex-wrap items-center justify-between gap-4">
  349. <div className="flex items-center space-x-2">
  350. <span>时间范围:</span>
  351. <Select
  352. defaultValue="today"
  353. onChange={handleTimeRangeChange}
  354. size="small"
  355. >
  356. {timeRangeOptions.map(option => (
  357. <Option key={option.value} value={option.value}>{option.label}</Option>
  358. ))}
  359. </Select>
  360. <RangePicker size="small" />
  361. </div>
  362. <div className="flex items-center space-x-4">
  363. <div className="flex items-center space-x-2">
  364. <span>自动刷新:</span>
  365. <Switch
  366. checked={autoRefresh}
  367. onChange={setAutoRefresh}
  368. size="small"
  369. />
  370. </div>
  371. <Button
  372. icon={<ReloadOutlined />}
  373. onClick={() => {
  374. updateStats()
  375. onButtonClick("手动刷新数据")
  376. }}
  377. size="small"
  378. >
  379. 刷新
  380. </Button>
  381. <Dropdown
  382. menu={{
  383. items: [
  384. { key: 'export-excel', label: '导出为Excel' },
  385. { key: 'export-pdf', label: '导出为PDF' },
  386. { key: 'export-image', label: '导出为图片' },
  387. ],
  388. onClick: ({ key }) => onButtonClick(`执行导出操作: ${key}`)
  389. }}
  390. >
  391. <Button icon={<ExportOutlined />} size="small">
  392. 导出 <DownOutlined />
  393. </Button>
  394. </Dropdown>
  395. </div>
  396. </div>
  397. </Card>
  398. {/* 统计卡片 */}
  399. <Row gutter={[16, 16]}>
  400. <Col span={6}>
  401. <Card
  402. className="hover:shadow-md transition-shadow cursor-pointer"
  403. onClick={() => onButtonClick("查看设备总数详情")}
  404. >
  405. <Statistic
  406. title="监测设备总数"
  407. value={deviceStats.total}
  408. valueStyle={{ color: "#3f8600" }}
  409. suffix="台"
  410. />
  411. <div className="mt-2">
  412. <Progress
  413. percent={Math.round((deviceStats.online / deviceStats.total) * 100)}
  414. size="small"
  415. status="normal"
  416. />
  417. <div className="text-xs text-gray-500 mt-1">
  418. 在线率: {Math.round((deviceStats.online / deviceStats.total) * 100)}%
  419. </div>
  420. </div>
  421. </Card>
  422. </Col>
  423. <Col span={6}>
  424. <Card
  425. className="hover:shadow-md transition-shadow cursor-pointer"
  426. onClick={() => onButtonClick("查看在线设备详情")}
  427. >
  428. <Statistic
  429. title="在线设备"
  430. value={deviceStats.online}
  431. valueStyle={{ color: "#1890ff" }}
  432. suffix="台"
  433. />
  434. <div className="mt-2 flex items-center">
  435. <Tag color="success">正常</Tag>
  436. <span className="text-xs text-gray-500 ml-2">
  437. {deviceStats.total - deviceStats.online} 台离线
  438. </span>
  439. </div>
  440. </Card>
  441. </Col>
  442. <Col span={6}>
  443. <Card
  444. className="hover:shadow-md transition-shadow cursor-pointer"
  445. onClick={() => onButtonClick("查看当前报警详情")}
  446. >
  447. <Statistic
  448. title="当前报警"
  449. value={deviceStats.alarms}
  450. valueStyle={{ color: deviceStats.alarms > 0 ? "#cf1322" : "#1890ff" }}
  451. suffix="条"
  452. />
  453. <div className="mt-2">
  454. <Badge
  455. status={deviceStats.alarms > 0 ? "error" : "success"}
  456. text={deviceStats.alarms > 0 ? "有报警" : "无报警"}
  457. />
  458. </div>
  459. </Card>
  460. </Col>
  461. <Col span={6}>
  462. <Card
  463. className="hover:shadow-md transition-shadow cursor-pointer"
  464. onClick={() => onButtonClick("查看易积水点详情")}
  465. >
  466. <Statistic
  467. title="易积水点"
  468. value={deviceStats.waterPoints}
  469. valueStyle={{ color: "#722ed1" }}
  470. suffix="个"
  471. />
  472. <div className="mt-2 flex items-center">
  473. <Tag color={deviceStats.alarms > 5 ? "error" : "warning"}>
  474. {deviceStats.alarms > 5 ? "高风险" : "中风险"}
  475. </Tag>
  476. </div>
  477. </Card>
  478. </Col>
  479. </Row>
  480. {/* 地图和图表 */}
  481. <Row gutter={[16, 16]}>
  482. <Col span={16}>
  483. <Card
  484. title="GIS"
  485. className="h-96"
  486. extra={
  487. <Space>
  488. <Button size="small" icon={<SettingOutlined />} onClick={() => onButtonClick("地图设置")}>
  489. 设置
  490. </Button>
  491. </Space>
  492. }
  493. >
  494. <GisMapBaidu height={'50'} />
  495. </Card>
  496. </Col>
  497. <Col span={8}>
  498. <Card
  499. title="实时监测数据"
  500. className="h-96"
  501. extra={
  502. <Dropdown
  503. menu={{
  504. items: [
  505. { key: 'line', label: '折线图', icon: <LineChartOutlined /> },
  506. { key: 'bar', label: '柱状图', icon: <BarChartOutlined /> },
  507. { key: 'pie', label: '饼图', icon: <PieChartOutlined /> },
  508. ],
  509. onClick: ({ key }) => onButtonClick(`切换图表类型: ${key}`)
  510. }}
  511. >
  512. <Button size="small" icon={<BarChartOutlined />}>
  513. 图表类型 <DownOutlined />
  514. </Button>
  515. </Dropdown>
  516. }
  517. >
  518. <MonitoringCharts />
  519. </Card>
  520. </Col>
  521. </Row>
  522. {/* 报警信息 */}
  523. <Card
  524. title="最新报警信息"
  525. extra={
  526. <Button type="link" onClick={() => onButtonClick("查看全部报警")}>
  527. 查看更多
  528. </Button>
  529. }
  530. >
  531. <AlarmList onButtonClick={onButtonClick} />
  532. </Card>
  533. </div>
  534. )
  535. }
  536. function RealTimeMonitoring({ onButtonClick }: { onButtonClick: (action: string) => void }) {
  537. const [filterStatus, setFilterStatus] = useState<string | null>(null)
  538. const [filterType, setFilterType] = useState<string | null>(null)
  539. const [searchText, setSearchText] = useState("")
  540. const deviceData = [
  541. {
  542. key: "1",
  543. deviceId: "FL001",
  544. deviceName: "主干道流量计",
  545. deviceType: "流量计",
  546. status: "online",
  547. currentValue: "2.5 m³/s",
  548. threshold: "3.0 m³/s",
  549. lastUpdate: "2024-01-15 14:29:30",
  550. },
  551. {
  552. key: "2",
  553. deviceId: "LV002",
  554. deviceName: "积水点液位计",
  555. deviceType: "液位计",
  556. status: "alarm",
  557. currentValue: "0.8 m",
  558. threshold: "0.5 m",
  559. lastUpdate: "2024-01-15 14:29:25",
  560. },
  561. {
  562. key: "3",
  563. deviceId: "PS001",
  564. deviceName: "第一泵站",
  565. deviceType: "泵站",
  566. status: "offline",
  567. currentValue: "-",
  568. threshold: "-",
  569. lastUpdate: "2024-01-15 14:25:10",
  570. },
  571. {
  572. key: "4",
  573. deviceId: "LV003",
  574. deviceName: "商业区液位计",
  575. deviceType: "液位计",
  576. status: "online",
  577. currentValue: "0.2 m",
  578. threshold: "0.5 m",
  579. lastUpdate: "2024-01-15 14:29:35",
  580. },
  581. ]
  582. const filteredData = deviceData.filter(device => {
  583. const matchesStatus = !filterStatus || device.status === filterStatus
  584. const matchesType = !filterType || device.deviceType === filterType
  585. const matchesSearch = !searchText ||
  586. device.deviceId.toLowerCase().includes(searchText.toLowerCase()) ||
  587. device.deviceName.toLowerCase().includes(searchText.toLowerCase())
  588. return matchesStatus && matchesType && matchesSearch
  589. })
  590. const clearFilters = () => {
  591. setFilterStatus(null)
  592. setFilterType(null)
  593. setSearchText("")
  594. }
  595. return (
  596. <div className="space-y-6">
  597. <Card title="设备运行实时监测">
  598. <div className="mb-4">
  599. <Space wrap>
  600. <Input
  601. placeholder="搜索设备编号或名称"
  602. prefix={<SearchOutlined />}
  603. value={searchText}
  604. onChange={e => setSearchText(e.target.value)}
  605. style={{ width: 200 }}
  606. />
  607. <Select
  608. placeholder="设备类型"
  609. style={{ width: 120 }}
  610. value={filterType}
  611. onChange={setFilterType}
  612. allowClear
  613. >
  614. <Option value="流量计">流量计</Option>
  615. <Option value="液位计">液位计</Option>
  616. <Option value="泵站">泵站</Option>
  617. </Select>
  618. <Select
  619. placeholder="运行状态"
  620. style={{ width: 120 }}
  621. value={filterStatus}
  622. onChange={setFilterStatus}
  623. allowClear
  624. >
  625. <Option value="online">在线</Option>
  626. <Option value="offline">离线</Option>
  627. <Option value="alarm">报警</Option>
  628. </Select>
  629. <Button
  630. icon={<FilterOutlined />}
  631. onClick={clearFilters}
  632. >
  633. 清除筛选
  634. </Button>
  635. <Button
  636. type="primary"
  637. icon={<SearchOutlined />}
  638. onClick={() => onButtonClick("查询设备")}
  639. >
  640. 查询
  641. </Button>
  642. <Button
  643. icon={<ExportOutlined />}
  644. onClick={() => onButtonClick("导出数据")}
  645. >
  646. 导出
  647. </Button>
  648. <Button
  649. icon={<ReloadOutlined />}
  650. onClick={() => onButtonClick("刷新数据")}
  651. >
  652. 刷新
  653. </Button>
  654. </Space>
  655. </div>
  656. <Table
  657. columns={[
  658. {
  659. title: "设备编号",
  660. dataIndex: "deviceId",
  661. key: "deviceId",
  662. sorter: (a, b) => a.deviceId.localeCompare(b.deviceId),
  663. },
  664. {
  665. title: "设备名称",
  666. dataIndex: "deviceName",
  667. key: "deviceName",
  668. sorter: (a, b) => a.deviceName.localeCompare(b.deviceName),
  669. },
  670. {
  671. title: "设备类型",
  672. dataIndex: "deviceType",
  673. key: "deviceType",
  674. filters: [
  675. { text: '流量计', value: '流量计' },
  676. { text: '液位计', value: '液位计' },
  677. { text: '泵站', value: '泵站' },
  678. ],
  679. onFilter: (value, record) => record.deviceType === value,
  680. },
  681. {
  682. title: "运行状态",
  683. dataIndex: "status",
  684. key: "status",
  685. filters: [
  686. { text: '在线', value: 'online' },
  687. { text: '离线', value: 'offline' },
  688. { text: '报警', value: 'alarm' },
  689. ],
  690. onFilter: (value, record) => record.status === value,
  691. render: (status: string) => (
  692. <Badge
  693. status={status === "online" ? "success" : status === "offline" ? "default" : "error"}
  694. text={status === "online" ? "在线" : status === "offline" ? "离线" : "报警"}
  695. />
  696. ),
  697. },
  698. {
  699. title: "当前值",
  700. dataIndex: "currentValue",
  701. key: "currentValue",
  702. sorter: (a, b) => {
  703. const aVal = parseFloat(a.currentValue) || 0
  704. const bVal = parseFloat(b.currentValue) || 0
  705. return aVal - bVal
  706. },
  707. },
  708. {
  709. title: "报警阈值",
  710. dataIndex: "threshold",
  711. key: "threshold"
  712. },
  713. {
  714. title: "最后更新",
  715. dataIndex: "lastUpdate",
  716. key: "lastUpdate",
  717. sorter: (a, b) => a.lastUpdate.localeCompare(b.lastUpdate),
  718. },
  719. {
  720. title: "操作",
  721. key: "action",
  722. render: (_, record) => (
  723. <Space>
  724. <Button
  725. size="small"
  726. onClick={() => onButtonClick(`查看设备详情: ${record.deviceName}`)}
  727. >
  728. 详情
  729. </Button>
  730. <Button
  731. size="small"
  732. onClick={() => onButtonClick(`设备定位: ${record.deviceName}`)}
  733. >
  734. 定位
  735. </Button>
  736. <Dropdown
  737. menu={{
  738. items: [
  739. { key: 'repair', label: '维修派单' },
  740. { key: 'history', label: '历史数据' },
  741. { key: 'settings', label: '设备设置' },
  742. ],
  743. onClick: ({ key }) => onButtonClick(`${key === 'repair' ? '维修派单' : key === 'history' ? '查看历史数据' : '设备设置'}: ${record.deviceName}`)
  744. }}
  745. >
  746. <Button size="small">
  747. 更多 <DownOutlined />
  748. </Button>
  749. </Dropdown>
  750. </Space>
  751. ),
  752. },
  753. ]}
  754. dataSource={filteredData}
  755. pagination={{
  756. pageSize: 10,
  757. showSizeChanger: true,
  758. showQuickJumper: true,
  759. }}
  760. scroll={{ x: 'max-content' }}
  761. />
  762. </Card>
  763. <Row gutter={[16, 16]}>
  764. <Col span={12}>
  765. <Card
  766. title="积水情况 GIS 一张图"
  767. extra={
  768. <Button
  769. size="small"
  770. onClick={() => onButtonClick("刷新积水地图")}
  771. >
  772. 刷新
  773. </Button>
  774. }
  775. >
  776. <MapView type="waterlogging" />
  777. </Card>
  778. </Col>
  779. <Col span={12}>
  780. <Card
  781. title="管网运行情况 GIS 一张图"
  782. extra={
  783. <Button
  784. size="small"
  785. onClick={() => onButtonClick("刷新管网地图")}
  786. >
  787. 刷新
  788. </Button>
  789. }
  790. >
  791. <MapView type="pipeline" />
  792. </Card>
  793. </Col>
  794. </Row>
  795. </div>
  796. )
  797. }
  798. function VideoMonitoringContent({
  799. onVideoClick,
  800. onButtonClick,
  801. onRefreshVideos,
  802. onVideoSettings
  803. }: {
  804. onVideoClick: () => void;
  805. onButtonClick: (action: string) => void;
  806. onRefreshVideos: () => void;
  807. onVideoSettings: () => void;
  808. }) {
  809. const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
  810. const [selectedVideos, setSelectedVideos] = useState<number[]>([])
  811. const videoData = [
  812. { id: 1, name: "人民路积水点监控", status: "online", location: "人民路与南京路交叉口" },
  813. { id: 2, name: "第一泵站监控", status: "online", location: "城东泵站" },
  814. { id: 3, name: "主干道管网监控", status: "offline", location: "淮海路主干管网" },
  815. { id: 4, name: "南京路积水点监控", status: "online", location: "南京路商业区" },
  816. { id: 5, name: "第二泵站监控", status: "alarm", location: "城西泵站" },
  817. { id: 6, name: "商业区管网监控", status: "online", location: "中心商业区" },
  818. { id: 7, name: "学校区域监控", status: "online", location: "第一中学附近" },
  819. { id: 8, name: "住宅区监控", status: "offline", location: "阳光小区" },
  820. ]
  821. const toggleVideoSelection = (id: number) => {
  822. setSelectedVideos(prev =>
  823. prev.includes(id)
  824. ? prev.filter(videoId => videoId !== id)
  825. : [...prev, id]
  826. )
  827. }
  828. const selectAllVideos = () => {
  829. setSelectedVideos(videoData.map(video => video.id))
  830. }
  831. const clearVideoSelection = () => {
  832. setSelectedVideos([])
  833. }
  834. return (
  835. <div className="space-y-6">
  836. <Card
  837. title="视频监控概览"
  838. extra={
  839. <Space>
  840. <Button
  841. icon={viewMode === 'grid' ? <LineChartOutlined /> : <BlockOutlined />}
  842. onClick={() => setViewMode(viewMode === 'grid' ? 'list' : 'grid')}
  843. >
  844. {viewMode === 'grid' ? '列表视图' : '网格视图'}
  845. </Button>
  846. </Space>
  847. }
  848. >
  849. <div className="mb-4">
  850. <Space wrap>
  851. <Button
  852. type="primary"
  853. onClick={onVideoClick}
  854. icon={<VideoCameraOutlined />}
  855. >
  856. 打开视频监控
  857. </Button>
  858. <Button
  859. icon={<ReloadOutlined />}
  860. onClick={() => {
  861. onRefreshVideos()
  862. globalMessage.success("视频列表已刷新")
  863. }}
  864. >
  865. 刷新列表
  866. </Button>
  867. <Button
  868. icon={<SettingOutlined />}
  869. onClick={() => {
  870. onVideoSettings()
  871. }}
  872. >
  873. 视频设置
  874. </Button>
  875. <Button
  876. onClick={selectAllVideos}
  877. >
  878. 全选
  879. </Button>
  880. <Button
  881. onClick={clearVideoSelection}
  882. >
  883. 清除选择
  884. </Button>
  885. {selectedVideos.length > 0 && (
  886. <span className="text-gray-500">
  887. 已选择 {selectedVideos.length} 个视频
  888. </span>
  889. )}
  890. </Space>
  891. </div>
  892. {viewMode === 'grid' ? (
  893. <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
  894. {videoData.map((video) => (
  895. <Card
  896. key={video.id}
  897. size="small"
  898. className={`cursor-pointer hover:shadow-md transition-all ${
  899. selectedVideos.includes(video.id) ? 'ring-2 ring-blue-500' : ''
  900. }`}
  901. onClick={() => toggleVideoSelection(video.id)}
  902. >
  903. <div className="aspect-video bg-gray-200 flex items-center justify-center relative">
  904. <VideoCameraOutlined className="text-2xl text-gray-400" />
  905. <div className="absolute top-2 right-2">
  906. <Badge
  907. status={video.status === "online" ? "success" : video.status === "alarm" ? "error" : "default"}
  908. />
  909. </div>
  910. </div>
  911. <div className="mt-2">
  912. <div className="font-medium text-sm flex justify-between">
  913. <span>{video.name}</span>
  914. {selectedVideos.includes(video.id) && (
  915. <CheckOutlined className="text-blue-500" />
  916. )}
  917. </div>
  918. <div className="text-xs text-gray-500">{video.location}</div>
  919. <div className="text-xs">
  920. <span className={
  921. video.status === "online"
  922. ? "text-green-600"
  923. : video.status === "alarm"
  924. ? "text-red-600"
  925. : "text-gray-400"
  926. }>
  927. {video.status === "online" ? "在线" : video.status === "alarm" ? "报警" : "离线"}
  928. </span>
  929. </div>
  930. </div>
  931. </Card>
  932. ))}
  933. </div>
  934. ) : (
  935. <Table
  936. dataSource={videoData}
  937. columns={[
  938. {
  939. title: '选择',
  940. key: 'selection',
  941. render: (_, record) => (
  942. <Checkbox
  943. checked={selectedVideos.includes(record.id)}
  944. onChange={() => toggleVideoSelection(record.id)}
  945. />
  946. ),
  947. },
  948. {
  949. title: '监控点名称',
  950. dataIndex: 'name',
  951. key: 'name',
  952. },
  953. {
  954. title: '位置',
  955. dataIndex: 'location',
  956. key: 'location',
  957. },
  958. {
  959. title: '状态',
  960. dataIndex: 'status',
  961. key: 'status',
  962. render: (status: string) => (
  963. <Badge
  964. status={status === "online" ? "success" : status === "alarm" ? "error" : "default"}
  965. text={status === "online" ? "在线" : status === "alarm" ? "报警" : "离线"}
  966. />
  967. ),
  968. },
  969. {
  970. title: '操作',
  971. key: 'action',
  972. render: (_, record) => (
  973. <Space>
  974. <Button
  975. size="small"
  976. type="primary"
  977. onClick={(e) => {
  978. e.stopPropagation()
  979. onVideoClick()
  980. }}
  981. >
  982. 查看
  983. </Button>
  984. <Button
  985. size="small"
  986. onClick={(e) => {
  987. e.stopPropagation()
  988. onButtonClick(`查看视频详情: ${record.name}`)
  989. }}
  990. >
  991. 详情
  992. </Button>
  993. </Space>
  994. ),
  995. },
  996. ]}
  997. rowSelection={{
  998. selectedRowKeys: selectedVideos,
  999. onChange: (selectedRowKeys) => {
  1000. setSelectedVideos(selectedRowKeys.map(key => Number(key)))
  1001. },
  1002. }}
  1003. />
  1004. )}
  1005. </Card>
  1006. </div>
  1007. )
  1008. }
  1009. function AlarmList({ onButtonClick }: { onButtonClick: (action: string) => void }) {
  1010. const [alarmData] = useState([
  1011. {
  1012. key: "1",
  1013. time: "2024-01-15 14:25:30",
  1014. device: "LV002",
  1015. location: "人民路积水点",
  1016. level: "高",
  1017. message: "液位超过报警阈值",
  1018. status: "未处理",
  1019. },
  1020. {
  1021. key: "2",
  1022. time: "2024-01-15 14:20:15",
  1023. device: "FL001",
  1024. location: "主干道流量计",
  1025. level: "中",
  1026. message: "流量异常波动",
  1027. status: "处理中",
  1028. },
  1029. {
  1030. key: "3",
  1031. time: "2024-01-15 14:15:45",
  1032. device: "PS001",
  1033. location: "第一泵站",
  1034. level: "高",
  1035. message: "设备离线",
  1036. status: "未处理",
  1037. },
  1038. {
  1039. key: "4",
  1040. time: "2024-01-15 14:10:20",
  1041. device: "LV003",
  1042. location: "商业区液位计",
  1043. level: "低",
  1044. message: "电池电量低",
  1045. status: "已处理",
  1046. },
  1047. ])
  1048. const [filteredAlarms, setFilteredAlarms] = useState(alarmData)
  1049. const [statusFilter, setStatusFilter] = useState<string | null>(null)
  1050. useEffect(() => {
  1051. if (statusFilter) {
  1052. setFilteredAlarms(alarmData.filter(alarm => alarm.status === statusFilter))
  1053. } else {
  1054. setFilteredAlarms(alarmData)
  1055. }
  1056. }, [statusFilter, alarmData])
  1057. return (
  1058. <div>
  1059. <div className="mb-4 flex justify-between">
  1060. <Space>
  1061. <Select
  1062. placeholder="处理状态"
  1063. style={{ width: 120 }}
  1064. onChange={setStatusFilter}
  1065. allowClear
  1066. value={statusFilter}
  1067. >
  1068. <Option value="未处理">未处理</Option>
  1069. <Option value="处理中">处理中</Option>
  1070. <Option value="已处理">已处理</Option>
  1071. </Select>
  1072. <Button onClick={() => setStatusFilter(null)}>清除筛选</Button>
  1073. </Space>
  1074. <Button type="primary" onClick={() => onButtonClick("刷新报警列表")}>
  1075. 刷新
  1076. </Button>
  1077. </div>
  1078. <Table
  1079. columns={[
  1080. {
  1081. title: "报警时间",
  1082. dataIndex: "time",
  1083. key: "time",
  1084. sorter: (a, b) => a.time.localeCompare(b.time),
  1085. },
  1086. {
  1087. title: "设备编号",
  1088. dataIndex: "device",
  1089. key: "device"
  1090. },
  1091. {
  1092. title: "位置",
  1093. dataIndex: "location",
  1094. key: "location"
  1095. },
  1096. {
  1097. title: "报警级别",
  1098. dataIndex: "level",
  1099. key: "level",
  1100. filters: [
  1101. { text: '高', value: '高' },
  1102. { text: '中', value: '中' },
  1103. { text: '低', value: '低' },
  1104. ],
  1105. onFilter: (value, record) => record.level === value,
  1106. render: (level: string) => (
  1107. <Badge
  1108. color={level === "高" ? "red" : level === "中" ? "orange" : "blue"}
  1109. text={level}
  1110. />
  1111. ),
  1112. },
  1113. {
  1114. title: "报警信息",
  1115. dataIndex: "message",
  1116. key: "message"
  1117. },
  1118. {
  1119. title: "处理状态",
  1120. dataIndex: "status",
  1121. key: "status",
  1122. filters: [
  1123. { text: '未处理', value: '未处理' },
  1124. { text: '处理中', value: '处理中' },
  1125. { text: '已处理', value: '已处理' },
  1126. ],
  1127. onFilter: (value, record) => record.status === value,
  1128. render: (status: string) => (
  1129. <Badge
  1130. status={status === "未处理" ? "error" : status === "处理中" ? "processing" : "success"}
  1131. text={status}
  1132. />
  1133. ),
  1134. },
  1135. {
  1136. title: "操作",
  1137. key: "action",
  1138. render: (_, record) => (
  1139. <Space>
  1140. <Button
  1141. size="small"
  1142. onClick={() => onButtonClick(`查看报警详情: ${record.device}`)}
  1143. >
  1144. 详情
  1145. </Button>
  1146. {record.status !== "已处理" && (
  1147. <Button
  1148. size="small"
  1149. type="primary"
  1150. onClick={() => onButtonClick(`处理报警: ${record.device}`)}
  1151. >
  1152. 处理
  1153. </Button>
  1154. )}
  1155. </Space>
  1156. ),
  1157. },
  1158. ]}
  1159. dataSource={filteredAlarms}
  1160. pagination={{
  1161. pageSize: 5,
  1162. showSizeChanger: true,
  1163. showQuickJumper: true,
  1164. }}
  1165. size="small"
  1166. />
  1167. </div>
  1168. )
  1169. }