page.tsx 38 KB

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