page.tsx 66 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646
  1. "use client"
  2. import "antd/dist/reset.css"
  3. import {useEffect, useMemo, useRef, useState} from "react"
  4. import dynamic from "next/dynamic"
  5. import type {MenuProps} from "antd"
  6. import {
  7. Badge,
  8. Button,
  9. Card,
  10. Col,
  11. Collapse,
  12. Descriptions,
  13. Divider,
  14. Drawer,
  15. Form,
  16. Input,
  17. Layout,
  18. Menu,
  19. Modal,
  20. Popover,
  21. Progress,
  22. Row,
  23. Segmented,
  24. Select,
  25. Slider,
  26. Space,
  27. Statistic,
  28. Steps,
  29. Switch,
  30. Table,
  31. Tabs,
  32. Tag,
  33. Timeline,
  34. Typography,
  35. Upload,
  36. } from "antd"
  37. import {
  38. Activity,
  39. Bell,
  40. Box,
  41. Boxes,
  42. Check,
  43. CheckCircle2,
  44. CloudUpload,
  45. Cog,
  46. Database,
  47. Factory,
  48. FileJson2,
  49. Globe,
  50. Layers,
  51. LinkIcon,
  52. Lock,
  53. Network,
  54. Package,
  55. Radar,
  56. RefreshCw,
  57. Send,
  58. Server,
  59. Settings,
  60. ShieldCheck,
  61. SignalHigh,
  62. Sprout,
  63. UploadIcon,
  64. Users,
  65. } from "lucide-react"
  66. import globalMessage from "@/app/_modules/globalMessage";
  67. const { Header, Sider, Content } = Layout
  68. const { TextArea } = Input
  69. const { Title, Text } = Typography
  70. // Lazy components that require browser APIs
  71. const DeviceMap = dynamic(() => import("./components/device-map"), { ssr: false })
  72. const RealtimeLine = dynamic(() => import("./components/realtime-line"), { ssr: false })
  73. const QueueGauge = dynamic(() => import("./components/queue-gauge"), { ssr: false })
  74. type Project = {
  75. id: string
  76. name: string
  77. description?: string
  78. createdAt: string
  79. }
  80. type Product = {
  81. id: string
  82. name: string
  83. protocol: "TCP" | "UDP" | "LWM2M" | "MQTTS" | "HTTP/HTTPS"
  84. projectId: string
  85. thirdParty: boolean
  86. authToken?: string
  87. }
  88. type ThingModel = {
  89. id: string
  90. productId: string
  91. json: string
  92. }
  93. type Device = {
  94. id: string
  95. name: string
  96. productId: string
  97. projectId: string
  98. status: "online" | "offline"
  99. lastSeen?: string
  100. lat: number
  101. lng: number
  102. }
  103. type Telemetry = {
  104. ts: number
  105. value: number
  106. deviceId: string
  107. }
  108. const initialProjects: Project[] = [
  109. { id: "p1", name: "城市燃气", description: "管网与阀井监测", createdAt: new Date().toISOString() },
  110. { id: "p2", name: "城市水务", description: "供排水管线监测", createdAt: new Date().toISOString() },
  111. ]
  112. const initialProducts: Product[] = [
  113. { id: "prod1", name: "压力传感器A", protocol: "MQTTS", projectId: "p1", thirdParty: false },
  114. { id: "prod2", name: "井盖监测B", protocol: "HTTP/HTTPS", projectId: "p1", thirdParty: true, authToken: "TP-XXXX" },
  115. { id: "prod3", name: "流量计C", protocol: "LWM2M", projectId: "p2", thirdParty: false },
  116. ]
  117. const initialThingModels: ThingModel[] = [
  118. {
  119. id: "tm1",
  120. productId: "prod1",
  121. json: JSON.stringify(
  122. {
  123. properties: [
  124. { id: "pressure", name: "压力", type: "number", unit: "kPa" },
  125. { id: "battery", name: "电量", type: "number", unit: "%" },
  126. ],
  127. events: [{ id: "alarm", name: "告警", level: "high" }],
  128. services: [{ id: "reset", name: "复位", in: {}, out: {} }],
  129. },
  130. null,
  131. 2,
  132. ),
  133. },
  134. ]
  135. const initialDevices: Device[] = [
  136. {
  137. id: "dev-001",
  138. name: "压力A-001",
  139. productId: "prod1",
  140. projectId: "p1",
  141. status: "online",
  142. lastSeen: new Date().toISOString(),
  143. lat: 31.231706,
  144. lng: 121.472644,
  145. },
  146. {
  147. id: "dev-002",
  148. name: "井盖B-002",
  149. productId: "prod2",
  150. projectId: "p1",
  151. status: "offline",
  152. lat: 31.220706,
  153. lng: 121.462644,
  154. },
  155. {
  156. id: "dev-003",
  157. name: "流量C-003",
  158. productId: "prod3",
  159. projectId: "p2",
  160. status: "online",
  161. lastSeen: new Date().toISOString(),
  162. lat: 31.241706,
  163. lng: 121.482644,
  164. },
  165. ]
  166. function useInterval(callback: () => void, delay: number | null) {
  167. const savedRef = useRef<() => void>(null)
  168. useEffect(() => {
  169. savedRef.current = callback
  170. }, [callback])
  171. useEffect(() => {
  172. if (delay === null) return
  173. const id = setInterval(() => savedRef.current && savedRef.current(), delay)
  174. return () => clearInterval(id)
  175. }, [delay])
  176. }
  177. export default function Page() {
  178. const [collapsed, setCollapsed] = useState(false)
  179. const [activeKey, setActiveKey] = useState<string>("overview")
  180. // Core states
  181. const [projects, setProjects] = useState<Project[]>(initialProjects)
  182. const [products, setProducts] = useState<Product[]>(initialProducts)
  183. const [thingModels, setThingModels] = useState<ThingModel[]>(initialThingModels)
  184. const [devices, setDevices] = useState<Device[]>(initialDevices)
  185. const [telemetry, setTelemetry] = useState<Telemetry[]>([])
  186. const [backlog, setBacklog] = useState<number>(8)
  187. const [throughput, setThroughput] = useState<number>(120)
  188. const [alerts, setAlerts] = useState<number>(1)
  189. const [todayCount, setTodayCount] = useState<number>(3200)
  190. const [logs, setLogs] = useState<
  191. { key: string; time: string; deviceId: string; product: string; msg: string; level: "INFO" | "WARN" | "ERROR" }[]
  192. >([])
  193. // UI states for feedback
  194. const [projectModalOpen, setProjectModalOpen] = useState(false)
  195. const [productModalOpen, setProductModalOpen] = useState(false)
  196. const [deviceModalOpen, setDeviceModalOpen] = useState(false)
  197. const [cmdModal, setCmdModal] = useState<{ open: boolean; device?: Device }>({ open: false })
  198. const [dataDrawer, setDataDrawer] = useState<{ open: boolean; device?: Device }>({ open: false })
  199. const [registerDrawer, setRegisterDrawer] = useState(false)
  200. const [notifOpen, setNotifOpen] = useState(false)
  201. // Extra feedback modals
  202. const [svcModal, setSvcModal] = useState<{ open: boolean; svc?: any }>({ open: false })
  203. const [weightModal, setWeightModal] = useState<{ open: boolean; weight: number }>({ open: false, weight: 50 })
  204. const [scheduleModal, setScheduleModal] = useState<boolean>(false)
  205. const [apiAccessModal, setApiAccessModal] = useState<boolean>(false)
  206. const [rbacModal, setRbacModal] = useState<{ open: boolean; user?: any }>({ open: false })
  207. const [form] = Form.useForm()
  208. const [productForm] = Form.useForm()
  209. const [deviceForm] = Form.useForm()
  210. const [cmdForm] = Form.useForm()
  211. // Simulate telemetry stream, queue and device health
  212. useInterval(() => {
  213. const onlineDevices = devices.filter((d) => d.status === "online")
  214. if (onlineDevices.length === 0) return
  215. const randDevice = onlineDevices[Math.floor(Math.random() * onlineDevices.length)]
  216. const v = Math.round(80 + Math.random() * 40 - 20)
  217. const now = Date.now()
  218. setTelemetry((prev) => {
  219. const next = [...prev, { ts: now, value: v, deviceId: randDevice.id }].slice(-120)
  220. return next
  221. })
  222. setTodayCount((c) => c + 1)
  223. setBacklog((b) => Math.max(0, b + 1 - Math.floor(throughput / 100)))
  224. setLogs((prev) => {
  225. const product = products.find((p) => p.id === randDevice.productId)?.name ?? "-"
  226. const next = [
  227. {
  228. key: `${now}`,
  229. time: new Date(now).toLocaleTimeString(),
  230. deviceId: randDevice.id,
  231. product,
  232. msg: `上报: value=${v}`,
  233. level: "INFO" as const,
  234. },
  235. ...prev,
  236. ].slice(0, 50)
  237. return next
  238. })
  239. if (Math.random() < 0.03) {
  240. setAlerts((a) => a + 1)
  241. setLogs((prev) => [
  242. {
  243. key: `${now}-warn`,
  244. time: new Date(now).toLocaleTimeString(),
  245. deviceId: randDevice.id,
  246. product: products.find((p) => p.id === randDevice.productId)?.name ?? "-",
  247. msg: "告警: 压力异常",
  248. level: "WARN",
  249. },
  250. ...prev,
  251. ])
  252. }
  253. setDevices((prev) => prev.map((d) => (d.id === randDevice.id ? { ...d, lastSeen: new Date().toISOString() } : d)))
  254. }, 1200)
  255. const onlineCount = devices.filter((d) => d.status === "online").length
  256. const menuItems: MenuProps["items"] = [
  257. { key: "overview", icon: <Activity size={18} />, label: "总览" },
  258. { key: "south", icon: <Server size={18} />, label: "南向接入" },
  259. { key: "terminal", icon: <LinkIcon size={18} />, label: "终端接入" },
  260. { key: "distribution", icon: <Send size={18} />, label: "数据分发" },
  261. { key: "mq", icon: <Network size={18} />, label: "北向消息队列" },
  262. { key: "warehouse", icon: <Database size={18} />, label: "数据仓库" },
  263. { key: "openapi", icon: <Globe size={18} />, label: "数据共享开放" },
  264. { key: "micro", icon: <Boxes size={18} />, label: "微服务" },
  265. { key: "auth", icon: <ShieldCheck size={18} />, label: "认证鉴权" },
  266. ]
  267. const projectOptions = projects.map((p) => ({ label: p.name, value: p.id }))
  268. const productOptions = products.map((p) => ({ label: p.name, value: p.id }))
  269. function addProject(values: any) {
  270. const p: Project = {
  271. id: `p-${Math.random().toString(36).slice(2, 8)}`,
  272. name: values.name,
  273. description: values.description,
  274. createdAt: new Date().toISOString(),
  275. }
  276. setProjects((prev) => [p, ...prev])
  277. setProjectModalOpen(false)
  278. form.resetFields()
  279. globalMessage.success("项目已创建")
  280. }
  281. function addProduct(values: any) {
  282. const prod: Product = {
  283. id: `prod-${Math.random().toString(36).slice(2, 8)}`,
  284. name: values.name,
  285. protocol: values.protocol,
  286. projectId: values.projectId,
  287. thirdParty: values.thirdParty || false,
  288. authToken: values.thirdParty ? `TP-${Math.random().toString(36).slice(2, 8)}` : undefined,
  289. }
  290. setProducts((prev) => [prod, ...prev])
  291. setProductModalOpen(false)
  292. productForm.resetFields()
  293. globalMessage.success("产品已创建")
  294. }
  295. function addDevice(values: any) {
  296. const dev: Device = {
  297. id: values.id,
  298. name: values.name,
  299. productId: values.productId,
  300. projectId: values.projectId,
  301. status: "offline",
  302. lat: values.lat ?? 31.23 + Math.random() * 0.05,
  303. lng: values.lng ?? 121.47 + Math.random() * 0.05,
  304. }
  305. setDevices((prev) => [dev, ...prev])
  306. setDeviceModalOpen(false)
  307. deviceForm.resetFields()
  308. globalMessage.success("设备已注册")
  309. }
  310. function toggleDevice(id: string, online: boolean) {
  311. setDevices((prev) =>
  312. prev.map((d) =>
  313. d.id === id
  314. ? { ...d, status: online ? "online" : "offline", lastSeen: online ? new Date().toISOString() : d.lastSeen }
  315. : d,
  316. ),
  317. )
  318. globalMessage.success(online ? "设备上线成功" : "设备已下线")
  319. }
  320. function sendCommand(values: any) {
  321. const device = cmdModal.device
  322. if (!device) return
  323. const now = Date.now()
  324. setLogs((prev) => [
  325. {
  326. key: `${now}-cmd`,
  327. time: new Date(now).toLocaleTimeString(),
  328. deviceId: device.id,
  329. product: products.find((p) => p.id === device.productId)?.name ?? "-",
  330. msg: `下发指令: ${values.serviceId} ${values.payload}`,
  331. level: "INFO",
  332. },
  333. ...prev,
  334. ])
  335. setCmdModal({ open: false })
  336. cmdForm.resetFields()
  337. globalMessage.success("指令已发送")
  338. }
  339. const telemetrySeries = useMemo(() => {
  340. const xs = telemetry.map((t) => new Date(t.ts).toLocaleTimeString())
  341. const ys = telemetry.map((t) => t.value)
  342. return { xs, ys }
  343. }, [telemetry])
  344. const alertLogs = logs.filter((l) => l.level !== "INFO").slice(0, 10)
  345. return (
  346. <Layout style={{ minHeight: "100vh" }}>
  347. <Sider collapsible collapsed={collapsed} onCollapse={setCollapsed} width={240}>
  348. <div style={{ height: 60, display: "flex", alignItems: "center", padding: "0 12px", gap: 8 }}>
  349. <Sprout size={22} color="white" />
  350. {!collapsed && <Text style={{ color: "white", fontWeight: 600 }}>生命线物联网平台</Text>}
  351. </div>
  352. <Menu
  353. theme="dark"
  354. mode="inline"
  355. selectedKeys={[activeKey]}
  356. items={menuItems}
  357. onClick={(e) => setActiveKey(e.key)}
  358. />
  359. </Sider>
  360. <Layout>
  361. <Header
  362. style={{
  363. background: "white",
  364. borderBottom: "1px solid #f0f0f0",
  365. display: "flex",
  366. alignItems: "center",
  367. padding: "0 16px",
  368. gap: 12,
  369. }}
  370. >
  371. <Radar size={22} />
  372. <Title level={4} style={{ margin: 0 }}>
  373. 城市生命线物联网平台
  374. </Title>
  375. <div style={{ marginLeft: "auto", display: "flex", alignItems: "center", gap: 12 }}>
  376. <Badge count={alerts} overflowCount={99}>
  377. <Button icon={<Bell size={16} />} onClick={() => setNotifOpen(true)} aria-label="查看通知" />
  378. </Badge>
  379. <Button icon={<RefreshCw size={16} />} onClick={() => globalMessage.success("已刷新")} aria-label="刷新" />
  380. <Button type="primary" icon={<CloudUpload size={16} />} onClick={() => setRegisterDrawer(true)}>
  381. 设备注册
  382. </Button>
  383. </div>
  384. </Header>
  385. <Content style={{ padding: 16 }}>
  386. {activeKey === "overview" && (
  387. <div className="space-y-4">
  388. <Row gutter={[16, 16]}>
  389. <Col xs={12} md={6}>
  390. <Card>
  391. <Statistic title="设备在线" value={onlineCount} prefix={<SignalHigh size={16} />} />
  392. </Card>
  393. </Col>
  394. <Col xs={12} md={6}>
  395. <Card>
  396. <Statistic title="今日上报" value={todayCount} prefix={<UploadIcon size={16} />} />
  397. </Card>
  398. </Col>
  399. <Col xs={12} md={6}>
  400. <Card>
  401. <Statistic title="队列积压" value={backlog} suffix="条" prefix={<Layers size={16} />} />
  402. </Card>
  403. </Col>
  404. <Col xs={12} md={6}>
  405. <Card>
  406. <Statistic title="告警数" value={alerts} prefix={<AlertIcon />} />
  407. </Card>
  408. </Col>
  409. </Row>
  410. <Row gutter={[16, 16]}>
  411. <Col xs={24} lg={16}>
  412. <Card title="实时监测趋势">
  413. <RealtimeLine title="压力/流量趋势" xs={telemetrySeries.xs} ys={telemetrySeries.ys} />
  414. </Card>
  415. </Col>
  416. <Col xs={24} lg={8}>
  417. <Card title="消息队列压力">
  418. <QueueGauge backlog={backlog} throughput={throughput} />
  419. <Divider />
  420. <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
  421. <Text>处理吞吐</Text>
  422. <Segmented
  423. options={[
  424. { label: "低", value: 80 },
  425. { label: "中", value: 120 },
  426. { label: "高", value: 200 },
  427. ]}
  428. value={throughput}
  429. onChange={(v) => {
  430. setThroughput(Number(v))
  431. globalMessage.success(`已切换吞吐:${v}/s`)
  432. }}
  433. />
  434. </div>
  435. </Card>
  436. </Col>
  437. </Row>
  438. <Row gutter={[16, 16]}>
  439. <Col xs={24} lg={12}>
  440. <Card title="设备分布地图">
  441. <div style={{ height: 360 }}>
  442. <DeviceMap devices={devices} />
  443. </div>
  444. </Card>
  445. </Col>
  446. <Col xs={24} lg={12}>
  447. <Card title="实时日志">
  448. <Table
  449. size="small"
  450. dataSource={logs}
  451. pagination={{ pageSize: 6 }}
  452. columns={[
  453. { title: "时间", dataIndex: "time", width: 100 },
  454. { title: "设备ID", dataIndex: "deviceId", width: 120 },
  455. { title: "产品", dataIndex: "product", width: 120 },
  456. {
  457. title: "级别",
  458. dataIndex: "level",
  459. width: 80,
  460. render: (lv) =>
  461. lv === "INFO" ? (
  462. <Tag color="blue">INFO</Tag>
  463. ) : lv === "WARN" ? (
  464. <Tag color="orange">WARN</Tag>
  465. ) : (
  466. <Tag color="red">ERROR</Tag>
  467. ),
  468. },
  469. { title: "消息", dataIndex: "msg" },
  470. ]}
  471. />
  472. </Card>
  473. </Col>
  474. </Row>
  475. </div>
  476. )}
  477. {activeKey === "south" && (
  478. <Card>
  479. <Tabs
  480. defaultActiveKey="projects"
  481. items={[
  482. {
  483. key: "projects",
  484. label: (
  485. <div className="flex items-center gap-2">
  486. <Package size={16} /> 项目管理
  487. </div>
  488. ),
  489. children: (
  490. <>
  491. <div className="flex justify-between mb-3">
  492. <Text type="secondary">按项目分类设备,方便资产统一管理</Text>
  493. <Button type="primary" onClick={() => setProjectModalOpen(true)} icon={<PlusIcon />}>
  494. 新建项目
  495. </Button>
  496. </div>
  497. <Table<Project>
  498. rowKey="id"
  499. dataSource={projects}
  500. columns={[
  501. { title: "项目名称", dataIndex: "name" },
  502. { title: "描述", dataIndex: "description" },
  503. { title: "创建时间", dataIndex: "createdAt", render: (t) => new Date(t).toLocaleString() },
  504. {
  505. title: "设备数",
  506. render: (_, r) => devices.filter((d) => d.projectId === r.id).length,
  507. },
  508. ]}
  509. />
  510. <Modal
  511. title="新建项目"
  512. open={projectModalOpen}
  513. onCancel={() => setProjectModalOpen(false)}
  514. onOk={() => form.submit()}
  515. okText="保存"
  516. >
  517. <Form form={form} layout="vertical" onFinish={addProject}>
  518. <Form.Item label="项目名称" name="name" rules={[{ required: true, message: "请输入名称" }]}>
  519. <Input placeholder="例如:城市燃气" />
  520. </Form.Item>
  521. <Form.Item label="描述" name="description">
  522. <Input placeholder="可选" />
  523. </Form.Item>
  524. </Form>
  525. </Modal>
  526. </>
  527. ),
  528. },
  529. {
  530. key: "products",
  531. label: (
  532. <div className="flex items-center gap-2">
  533. <Box size={16} /> 产品管理
  534. </div>
  535. ),
  536. children: (
  537. <>
  538. <div className="flex justify-between mb-3">
  539. <Text type="secondary">定义产品、添加设备,管理自建与第三方授权产品</Text>
  540. <Button type="primary" icon={<PlusIcon />} onClick={() => setProductModalOpen(true)}>
  541. 新建产品
  542. </Button>
  543. </div>
  544. <Table<Product>
  545. rowKey="id"
  546. dataSource={products}
  547. columns={[
  548. { title: "名称", dataIndex: "name" },
  549. { title: "协议", dataIndex: "protocol" },
  550. {
  551. title: "归属项目",
  552. dataIndex: "projectId",
  553. render: (id) => projects.find((p) => p.id === id)?.name,
  554. },
  555. {
  556. title: "类型",
  557. dataIndex: "thirdParty",
  558. render: (v: boolean) =>
  559. v ? <Tag color="purple">第三方</Tag> : <Tag color="green">自建</Tag>,
  560. },
  561. {
  562. title: "授权Token",
  563. dataIndex: "authToken",
  564. render: (t) => (t ? <Text code>{t}</Text> : <Text type="secondary">-</Text>),
  565. },
  566. {
  567. title: "设备数",
  568. render: (_, r) => devices.filter((d) => d.productId === r.id).length,
  569. },
  570. ]}
  571. />
  572. <Modal
  573. title="新建产品"
  574. open={productModalOpen}
  575. onCancel={() => setProductModalOpen(false)}
  576. onOk={() => productForm.submit()}
  577. okText="保存"
  578. >
  579. <Form form={productForm} layout="vertical" onFinish={addProduct}>
  580. <Form.Item label="产品名称" name="name" rules={[{ required: true }]}>
  581. <Input placeholder="例如:压力传感器A" />
  582. </Form.Item>
  583. <Form.Item label="接入协议" name="protocol" rules={[{ required: true }]}>
  584. <Select
  585. options={[
  586. { label: "TCP", value: "TCP" },
  587. { label: "UDP", value: "UDP" },
  588. { label: "LWM2M", value: "LWM2M" },
  589. { label: "MQTTS", value: "MQTTS" },
  590. { label: "HTTP/HTTPS", value: "HTTP/HTTPS" },
  591. ]}
  592. />
  593. </Form.Item>
  594. <Form.Item label="归属项目" name="projectId" rules={[{ required: true }]}>
  595. <Select options={projectOptions} />
  596. </Form.Item>
  597. <Form.Item label="第三方产品" name="thirdParty" valuePropName="checked">
  598. <Switch />
  599. </Form.Item>
  600. </Form>
  601. </Modal>
  602. </>
  603. ),
  604. },
  605. {
  606. key: "thing",
  607. label: (
  608. <div className="flex items-center gap-2">
  609. <FileJson2 size={16} /> 物模型管理
  610. </div>
  611. ),
  612. children: (
  613. <>
  614. <Row gutter={[16, 16]}>
  615. <Col xs={24} md={12}>
  616. <Card
  617. title="定义物模型(JSON)"
  618. extra={
  619. <Button
  620. onClick={() => {
  621. const t: ThingModel = {
  622. id: `tm-${Math.random().toString(36).slice(2, 8)}`,
  623. productId: products[0]?.id ?? "",
  624. json: JSON.stringify(
  625. {
  626. properties: [{ id: "temp", name: "温度", type: "number", unit: "℃" }],
  627. events: [],
  628. services: [],
  629. },
  630. null,
  631. 2,
  632. ),
  633. }
  634. setThingModels((prev) => [t, ...prev])
  635. globalMessage.success("已创建示例物模型")
  636. }}
  637. >
  638. 创建示例
  639. </Button>
  640. }
  641. >
  642. <Table
  643. size="small"
  644. rowKey="id"
  645. dataSource={thingModels}
  646. pagination={{ pageSize: 5 }}
  647. columns={[
  648. { title: "ID", dataIndex: "id", width: 120 },
  649. {
  650. title: "产品",
  651. dataIndex: "productId",
  652. render: (id: string) => products.find((p) => p.id === id)?.name ?? "-",
  653. },
  654. {
  655. title: "属性数",
  656. render: (_, r: ThingModel) => {
  657. try {
  658. const o = JSON.parse(r.json)
  659. return o.properties?.length ?? 0
  660. } catch {
  661. return "-"
  662. }
  663. },
  664. },
  665. {
  666. title: "操作",
  667. render: (_, r: ThingModel) => (
  668. <Space>
  669. <Popover
  670. title="物模型预览"
  671. content={
  672. <pre style={{ maxWidth: 360, maxHeight: 200, overflow: "auto", margin: 0 }}>
  673. {r.json}
  674. </pre>
  675. }
  676. >
  677. <Button size="small">预览</Button>
  678. </Popover>
  679. <Button
  680. size="small"
  681. onClick={() => {
  682. const prod = products.find((p) => p.id === r.productId)
  683. globalMessage.success(`已应用到产品 ${prod?.name ?? "-"}`)
  684. }}
  685. >
  686. 应用到产品
  687. </Button>
  688. </Space>
  689. ),
  690. },
  691. ]}
  692. />
  693. </Card>
  694. </Col>
  695. <Col xs={24} md={12}>
  696. <Card title="快速校验">
  697. <Form
  698. layout="vertical"
  699. onFinish={(v) => {
  700. try {
  701. const obj = JSON.parse(v.json)
  702. if (!Array.isArray(obj.properties)) throw new Error("缺少 properties")
  703. globalMessage.success("物模型JSON校验通过")
  704. } catch (e: any) {
  705. globalMessage.error(`JSON错误: ${e.message}`)
  706. }
  707. }}
  708. >
  709. <Form.Item
  710. label="JSON定义"
  711. name="json"
  712. rules={[{ required: true, message: "请输入物模型JSON" }]}
  713. initialValue={thingModels[0]?.json}
  714. >
  715. <TextArea rows={12} placeholder="输入物模型JSON" />
  716. </Form.Item>
  717. <Button type="primary" htmlType="submit" icon={<Check size={16} />}>
  718. 校验
  719. </Button>
  720. </Form>
  721. </Card>
  722. </Col>
  723. </Row>
  724. </>
  725. ),
  726. },
  727. {
  728. key: "devices",
  729. label: (
  730. <div className="flex items-center gap-2">
  731. <Factory size={16} /> 设备管理
  732. </div>
  733. ),
  734. children: (
  735. <>
  736. <div className="flex justify-between mb-3">
  737. <Text type="secondary">注册、上线、上报数据与指令下发</Text>
  738. <Space>
  739. <Button icon={<PlusIcon />} onClick={() => setDeviceModalOpen(true)}>
  740. 注册设备
  741. </Button>
  742. <Button type="primary" icon={<Send size={16} />} onClick={() => setRegisterDrawer(true)}>
  743. 批量/页面注册
  744. </Button>
  745. </Space>
  746. </div>
  747. <Table<Device>
  748. rowKey="id"
  749. dataSource={devices}
  750. pagination={{ pageSize: 8 }}
  751. columns={[
  752. { title: "设备ID", dataIndex: "id", width: 140 },
  753. { title: "名称", dataIndex: "name" },
  754. {
  755. title: "项目",
  756. dataIndex: "projectId",
  757. render: (id) => projects.find((p) => p.id === id)?.name,
  758. },
  759. {
  760. title: "产品",
  761. dataIndex: "productId",
  762. render: (id) => products.find((p) => p.id === id)?.name,
  763. },
  764. {
  765. title: "状态",
  766. dataIndex: "status",
  767. render: (s: Device["status"]) =>
  768. s === "online" ? <Tag color="green">在线</Tag> : <Tag>离线</Tag>,
  769. },
  770. {
  771. title: "最后上报",
  772. dataIndex: "lastSeen",
  773. render: (t) => (t ? new Date(t).toLocaleString() : "-"),
  774. },
  775. {
  776. title: "操作",
  777. render: (_, r) => (
  778. <Space>
  779. {r.status === "online" ? (
  780. <Button size="small" onClick={() => toggleDevice(r.id, false)}>
  781. 下线
  782. </Button>
  783. ) : (
  784. <Button size="small" type="primary" onClick={() => toggleDevice(r.id, true)}>
  785. 上线
  786. </Button>
  787. )}
  788. <Button
  789. size="small"
  790. icon={<Send size={14} />}
  791. onClick={() => setCmdModal({ open: true, device: r })}
  792. >
  793. 下发指令
  794. </Button>
  795. <Button size="small" onClick={() => setDataDrawer({ open: true, device: r })}>
  796. 上报数据
  797. </Button>
  798. </Space>
  799. ),
  800. },
  801. ]}
  802. />
  803. <Modal
  804. title="注册设备"
  805. open={deviceModalOpen}
  806. onCancel={() => setDeviceModalOpen(false)}
  807. onOk={() => deviceForm.submit()}
  808. okText="保存"
  809. >
  810. <Form form={deviceForm} layout="vertical" onFinish={addDevice}>
  811. <Form.Item label="设备ID" name="id" rules={[{ required: true }]}>
  812. <Input placeholder="例如:dev-004" />
  813. </Form.Item>
  814. <Form.Item label="设备名称" name="name" rules={[{ required: true }]}>
  815. <Input placeholder="例如:压力A-004" />
  816. </Form.Item>
  817. <Form.Item label="项目" name="projectId" rules={[{ required: true }]}>
  818. <Select options={projectOptions} />
  819. </Form.Item>
  820. <Form.Item label="产品" name="productId" rules={[{ required: true }]}>
  821. <Select options={productOptions} />
  822. </Form.Item>
  823. <Row gutter={8}>
  824. <Col span={12}>
  825. <Form.Item label="纬度" name="lat">
  826. <Input type="number" placeholder="31.23" />
  827. </Form.Item>
  828. </Col>
  829. <Col span={12}>
  830. <Form.Item label="经度" name="lng">
  831. <Input type="number" placeholder="121.47" />
  832. </Form.Item>
  833. </Col>
  834. </Row>
  835. </Form>
  836. </Modal>
  837. <Modal
  838. title={`下发指令 - ${cmdModal.device?.name ?? ""}`}
  839. open={cmdModal.open}
  840. onCancel={() => setCmdModal({ open: false })}
  841. onOk={() => cmdForm.submit()}
  842. okText="发送"
  843. >
  844. <Form form={cmdForm} layout="vertical" onFinish={sendCommand}>
  845. <Form.Item label="服务标识" name="serviceId" rules={[{ required: true }]}>
  846. <Input placeholder="例如:reset" />
  847. </Form.Item>
  848. <Form.Item label="负载(JSON)" name="payload" rules={[{ required: true }]}>
  849. <TextArea rows={6} placeholder='例如:{"delay":5}' />
  850. </Form.Item>
  851. </Form>
  852. </Modal>
  853. <Drawer
  854. title={`上报数据 - ${dataDrawer.device?.name ?? ""}`}
  855. open={dataDrawer.open}
  856. onClose={() => setDataDrawer({ open: false })}
  857. width={520}
  858. >
  859. <Table
  860. size="small"
  861. pagination={{ pageSize: 10 }}
  862. dataSource={logs.filter((l) => l.deviceId === dataDrawer.device?.id)}
  863. columns={[
  864. { title: "时间", dataIndex: "time", width: 120 },
  865. { title: "级别", dataIndex: "level", width: 80 },
  866. { title: "消息", dataIndex: "msg" },
  867. ]}
  868. />
  869. </Drawer>
  870. </>
  871. ),
  872. },
  873. ]}
  874. />
  875. </Card>
  876. )}
  877. {activeKey === "terminal" && (
  878. <Row gutter={[16, 16]}>
  879. <Col xs={24} lg={14}>
  880. <Card title="设备连接与认证流程">
  881. <Steps
  882. direction="vertical"
  883. items={[
  884. {
  885. title: "选择协议连接",
  886. description: "支持 TCP / UDP / LWM2M / MQTTS / HTTP(S)",
  887. icon: <LinkIcon size={16} />,
  888. },
  889. {
  890. title: "设备注册(批量/单一)",
  891. description: "提供服务交互或页面交互两种方式",
  892. icon: <Users size={16} />,
  893. },
  894. {
  895. title: "授权与安全认证",
  896. description: "加解密与签名认证模块,确保高安全场景",
  897. icon: <Lock size={16} />,
  898. },
  899. {
  900. title: "上线与心跳",
  901. description: "接入后定期保活,保持在线状态",
  902. icon: <CheckCircle2 size={16} />,
  903. },
  904. ]}
  905. />
  906. <Divider />
  907. <Form
  908. layout="vertical"
  909. onFinish={() => {
  910. globalMessage.success("认证通过")
  911. }}
  912. >
  913. <Row gutter={12}>
  914. <Col span={12}>
  915. <Form.Item label="设备ID" name="id" rules={[{ required: true }]}>
  916. <Input placeholder="dev-xxx" />
  917. </Form.Item>
  918. </Col>
  919. <Col span={12}>
  920. <Form.Item label="协议" name="protocol" rules={[{ required: true }]}>
  921. <Select
  922. options={[
  923. { label: "TCP", value: "TCP" },
  924. { label: "UDP", value: "UDP" },
  925. { label: "LWM2M", value: "LWM2M" },
  926. { label: "MQTTS", value: "MQTTS" },
  927. { label: "HTTP/HTTPS", value: "HTTP/HTTPS" },
  928. ]}
  929. />
  930. </Form.Item>
  931. </Col>
  932. </Row>
  933. <Form.Item label="设备密钥" name="secret" rules={[{ required: true }]}>
  934. <Input.Password placeholder="******" />
  935. </Form.Item>
  936. <Button type="primary" htmlType="submit" icon={<ShieldCheck size={16} />}>
  937. 认证
  938. </Button>
  939. </Form>
  940. </Card>
  941. </Col>
  942. <Col xs={24} lg={10}>
  943. <Card title="批量注册(CSV)">
  944. <Upload.Dragger
  945. multiple
  946. beforeUpload={() => {
  947. globalMessage.success("CSV已接收,已加入处理队列")
  948. return false
  949. }}
  950. >
  951. <p className="ant-upload-drag-icon">
  952. <UploadIcon />
  953. </p>
  954. <p className="ant-upload-text">点击或拖拽CSV文件到此处上传</p>
  955. <p className="ant-upload-hint">示例列:deviceId,productId,projectId,secret</p>
  956. </Upload.Dragger>
  957. </Card>
  958. <Card title="连接参数示例" className="mt-4">
  959. <Descriptions size="small" column={1} bordered>
  960. <Descriptions.Item label="MQTTS 服务器">mqtts://iot.example.com:8883</Descriptions.Item>
  961. <Descriptions.Item label="HTTP 上报">POST https://api.example.com/iot/ingest</Descriptions.Item>
  962. <Descriptions.Item label="LWM2M 服务器">coaps://lwm2m.example.com:5684</Descriptions.Item>
  963. </Descriptions>
  964. </Card>
  965. </Col>
  966. </Row>
  967. )}
  968. {activeKey === "distribution" && (
  969. <Row gutter={[16, 16]}>
  970. <Col xs={24} md={12}>
  971. <Card title="数据接入与加工流程">
  972. <Timeline
  973. items={[
  974. {
  975. color: "blue",
  976. children: "协议接入(TCP/UDP/LWM2M/MQTTS/HTTP)",
  977. },
  978. {
  979. color: "green",
  980. children: "报文解析(TLV/JSON/自定义)",
  981. },
  982. {
  983. color: "green",
  984. children: "清洗与标准化(字段映射/单位换算/去噪)",
  985. },
  986. {
  987. color: "green",
  988. children: "入库(时序/对象/冷存)",
  989. },
  990. {
  991. color: "purple",
  992. children: "开放共享(发布/订阅)",
  993. },
  994. ]}
  995. />
  996. </Card>
  997. <Card title="实时发布/订阅" className="mt-4">
  998. <Space direction="vertical" style={{ width: "100%" }}>
  999. <div className="flex items-center justify-between">
  1000. <Text>发布主题</Text>
  1001. <Text code>{`/city/${products[0]?.id ?? "prod"}/pressure`}</Text>
  1002. </div>
  1003. <div className="flex items-center gap-8">
  1004. <div className="flex items-center gap-2">
  1005. <Text type="secondary">发布</Text>
  1006. <Switch defaultChecked onChange={(c) => globalMessage.success(c ? "已开启发布" : "已关闭发布")} />
  1007. </div>
  1008. <div className="flex items-center gap-2">
  1009. <Text type="secondary">订阅</Text>
  1010. <Switch defaultChecked onChange={(c) => globalMessage.success(c ? "已开启订阅" : "已关闭订阅")} />
  1011. </div>
  1012. <Button icon={<Send size={16} />} onClick={() => globalMessage.success("已发布一条消息")}>
  1013. 推送一条
  1014. </Button>
  1015. </div>
  1016. </Space>
  1017. </Card>
  1018. </Col>
  1019. <Col xs={24} md={12}>
  1020. <Card title="加工结果预览">
  1021. <pre
  1022. style={{
  1023. maxHeight: 320,
  1024. overflow: "auto",
  1025. background: "#0b1020",
  1026. color: "#d6e5ff",
  1027. padding: 12,
  1028. borderRadius: 8,
  1029. }}
  1030. >
  1031. {`{
  1032. "deviceId": "${devices[0]?.id ?? "dev"}",
  1033. "ts": "${new Date().toISOString()}",
  1034. "model": "pressure",
  1035. "value": ${Number.isFinite(telemetrySeries.ys.at(-1) ?? Number.NaN) ? telemetrySeries.ys.at(-1) : 100},
  1036. "unit": "kPa",
  1037. "quality": "good"
  1038. }`}
  1039. </pre>
  1040. </Card>
  1041. </Col>
  1042. </Row>
  1043. )}
  1044. {activeKey === "mq" && (
  1045. <Row gutter={[16, 16]}>
  1046. <Col xs={24} lg={10}>
  1047. <Card title="流量削峰与高并发处理">
  1048. <QueueGauge backlog={backlog} throughput={throughput} />
  1049. <Divider />
  1050. <Text>解耦采集与处理服务,实现高并发接入</Text>
  1051. <Progress className="mt-3" percent={Math.min(100, Math.round((throughput / 250) * 100))} steps={12} />
  1052. </Card>
  1053. <Card title="指标类型转换" className="mt-4">
  1054. <Form
  1055. layout="vertical"
  1056. onFinish={() => globalMessage.success("转换规则已保存")}
  1057. initialValues={{ topic: "按产品", format: "物模型", compress: false }}
  1058. >
  1059. <Form.Item label="主题维度" name="topic">
  1060. <Select
  1061. options={[
  1062. { label: "按产品", value: "按产品" },
  1063. { label: "按物模型指标", value: "按物模型指标" },
  1064. { label: "按项目", value: "按项目" },
  1065. ]}
  1066. />
  1067. </Form.Item>
  1068. <Form.Item label="格式转换" name="format">
  1069. <Select
  1070. options={[
  1071. { label: "物模型(标准)", value: "物模型" },
  1072. { label: "CSV", value: "CSV" },
  1073. { label: "自定义", value: "自定义" },
  1074. ]}
  1075. />
  1076. </Form.Item>
  1077. <Form.Item label="开启压缩" name="compress" valuePropName="checked">
  1078. <Switch />
  1079. </Form.Item>
  1080. <Button type="primary" htmlType="submit" icon={<Check size={16} />}>
  1081. 保存
  1082. </Button>
  1083. </Form>
  1084. </Card>
  1085. </Col>
  1086. <Col xs={24} lg={14}>
  1087. <Card title="转换前/后对比">
  1088. <Row gutter={12}>
  1089. <Col span={12}>
  1090. <Title level={5}>原始数据</Title>
  1091. <pre
  1092. style={{
  1093. background: "#0b1020",
  1094. color: "#d6e5ff",
  1095. padding: 12,
  1096. borderRadius: 8,
  1097. minHeight: 220,
  1098. }}
  1099. >
  1100. {`{"dev":"${devices[0]?.id ?? "dev"}","p":${telemetrySeries.ys.at(-1) ?? 100},"ts":${Date.now()}}`}
  1101. </pre>
  1102. </Col>
  1103. <Col span={12}>
  1104. <Title level={5}>标准化后</Title>
  1105. <pre
  1106. style={{
  1107. background: "#0b1020",
  1108. color: "#d6e5ff",
  1109. padding: 12,
  1110. borderRadius: 8,
  1111. minHeight: 220,
  1112. }}
  1113. >
  1114. {`{
  1115. "deviceId": "${devices[0]?.id ?? "dev"}",
  1116. "metrics": [{"id":"pressure","value": ${telemetrySeries.ys.at(-1) ?? 100}, "unit":"kPa"}],
  1117. "timestamp": "${new Date().toISOString()}"
  1118. }`}
  1119. </pre>
  1120. </Col>
  1121. </Row>
  1122. </Card>
  1123. </Col>
  1124. </Row>
  1125. )}
  1126. {activeKey === "warehouse" && (
  1127. <Row gutter={[16, 16]}>
  1128. <Col xs={24} lg={14}>
  1129. <Card title="原始数据(近50条)">
  1130. <Table
  1131. size="small"
  1132. dataSource={logs.map((l, i) => ({ ...l, idx: i }))}
  1133. pagination={{ pageSize: 8 }}
  1134. columns={[
  1135. { title: "#", dataIndex: "idx", width: 60 },
  1136. { title: "时间", dataIndex: "time", width: 120 },
  1137. { title: "设备ID", dataIndex: "deviceId", width: 140 },
  1138. { title: "消息", dataIndex: "msg" },
  1139. ]}
  1140. />
  1141. </Card>
  1142. </Col>
  1143. <Col xs={24} lg={10}>
  1144. <Card title="ETL 作业">
  1145. <Collapse
  1146. items={[
  1147. {
  1148. key: "extract",
  1149. label: "抽取",
  1150. children: <Text>从接入缓冲区读取原始数据,写入ODS层。</Text>,
  1151. },
  1152. {
  1153. key: "clean",
  1154. label: "清洗",
  1155. children: <Text>字段校验、缺失填充、单位换算、异常剔除。</Text>,
  1156. },
  1157. { key: "load", label: "加载", children: <Text>入库到时序与事实表,供分析与API服务。</Text> },
  1158. ]}
  1159. defaultActiveKey={["extract", "clean", "load"]}
  1160. />
  1161. <Divider />
  1162. <Space>
  1163. <Button type="primary" icon={<PlayIcon />} onClick={() => globalMessage.success("已触发 ETL 作业")}>
  1164. 运行一次
  1165. </Button>
  1166. <Button icon={<Settings size={16} />} onClick={() => setScheduleModal(true)}>
  1167. 计划配置
  1168. </Button>
  1169. </Space>
  1170. </Card>
  1171. </Col>
  1172. </Row>
  1173. )}
  1174. {activeKey === "openapi" && (
  1175. <Row gutter={[16, 16]}>
  1176. <Col xs={24} lg={12}>
  1177. <Card title="接口服务管理">
  1178. <Table
  1179. size="small"
  1180. rowKey="name"
  1181. dataSource={[
  1182. { name: "实时监测查询", path: "/api/metrics/realtime", status: "running" },
  1183. { name: "设备状态查询", path: "/api/devices/status", status: "running" },
  1184. { name: "历史数据查询", path: "/api/metrics/history", status: "stopped" },
  1185. ]}
  1186. columns={[
  1187. { title: "名称", dataIndex: "name" },
  1188. { title: "路径", dataIndex: "path", render: (p) => <Text code>{p}</Text> },
  1189. {
  1190. title: "状态",
  1191. dataIndex: "status",
  1192. render: (s) =>
  1193. s === "running" ? (
  1194. <Badge status="success" text="运行中" />
  1195. ) : (
  1196. <Badge status="default" text="已停止" />
  1197. ),
  1198. },
  1199. {
  1200. title: "操作",
  1201. render: (_, r) => (
  1202. <Space>
  1203. <Button size="small" onClick={() => setSvcModal({ open: true, svc: r })}>
  1204. 配置
  1205. </Button>
  1206. <Button
  1207. size="small"
  1208. type="primary"
  1209. onClick={() => globalMessage.success(r.status === "running" ? "已停止" : "已启动")}
  1210. >
  1211. {r.status === "running" ? "停止" : "启动"}
  1212. </Button>
  1213. </Space>
  1214. ),
  1215. },
  1216. ]}
  1217. />
  1218. </Card>
  1219. </Col>
  1220. <Col xs={24} lg={12}>
  1221. <Card title="接口定义示例">
  1222. <pre
  1223. style={{ background: "#0b1020", color: "#d6e5ff", padding: 12, borderRadius: 8, minHeight: 240 }}
  1224. >
  1225. {`GET /api/metrics/realtime?productId=prod1
  1226. Response
  1227. {
  1228. "code": 0,
  1229. "data": [
  1230. {"deviceId":"dev-001","metric":"pressure","value": 112.3,"unit":"kPa","ts":"${new Date().toISOString()}"}
  1231. ]
  1232. }`}
  1233. </pre>
  1234. </Card>
  1235. </Col>
  1236. </Row>
  1237. )}
  1238. {activeKey === "micro" && (
  1239. <Row gutter={[16, 16]}>
  1240. <Col xs={24} lg={12}>
  1241. <Card title="注册中心(服务发现/健康检查/负载均衡)">
  1242. <Table
  1243. size="small"
  1244. rowKey="name"
  1245. dataSource={[
  1246. {
  1247. name: "ingest-service",
  1248. addr: "10.0.0.2:7001",
  1249. status: Math.random() > 0.05 ? "UP" : "DOWN",
  1250. load: "32%",
  1251. },
  1252. { name: "parser-service", addr: "10.0.0.3:7002", status: "UP", load: "41%" },
  1253. { name: "mq-gateway", addr: "10.0.0.4:7003", status: "UP", load: "27%" },
  1254. { name: "api-service", addr: "10.0.0.5:7004", status: "UP", load: "36%" },
  1255. ]}
  1256. columns={[
  1257. { title: "服务", dataIndex: "name" },
  1258. { title: "地址", dataIndex: "addr" },
  1259. {
  1260. title: "状态",
  1261. dataIndex: "status",
  1262. render: (s) =>
  1263. s === "UP" ? <Badge status="success" text="UP" /> : <Badge status="error" text="DOWN" />,
  1264. },
  1265. { title: "负载", dataIndex: "load" },
  1266. {
  1267. title: "操作",
  1268. render: (_, r) => (
  1269. <Space>
  1270. <Button
  1271. size="small"
  1272. icon={<RefreshCw size={14} />}
  1273. onClick={() => globalMessage.success(`已触发健康检查:${r.name}`)}
  1274. >
  1275. 健康检查
  1276. </Button>
  1277. <Button size="small" onClick={() => setWeightModal({ open: true, weight: 50 })}>
  1278. 权重
  1279. </Button>
  1280. </Space>
  1281. ),
  1282. },
  1283. ]}
  1284. />
  1285. </Card>
  1286. </Col>
  1287. <Col xs={24} lg={12}>
  1288. <Card title="配置中心(动态配置/多环境)">
  1289. <Form
  1290. layout="vertical"
  1291. onFinish={() => globalMessage.success("配置已更新")}
  1292. initialValues={{ env: "dev", log: "info", db: "postgres://neon-db/..." }}
  1293. >
  1294. <Form.Item label="环境" name="env">
  1295. <Select
  1296. options={[
  1297. { label: "开发", value: "dev" },
  1298. { label: "测试", value: "test" },
  1299. { label: "生产", value: "prod" },
  1300. ]}
  1301. />
  1302. </Form.Item>
  1303. <Form.Item label="日志级别" name="log">
  1304. <Select
  1305. options={[
  1306. { label: "debug", value: "debug" },
  1307. { label: "info", value: "info" },
  1308. { label: "warn", value: "warn" },
  1309. { label: "error", value: "error" },
  1310. ]}
  1311. />
  1312. </Form.Item>
  1313. <Form.Item label="数据库连接串" name="db">
  1314. <Input placeholder="postgres://..." />
  1315. </Form.Item>
  1316. <Space>
  1317. <Button type="primary" htmlType="submit" icon={<Check size={16} />}>
  1318. 保存并推送
  1319. </Button>
  1320. <Button icon={<Cog size={16} />} onClick={() => setApiAccessModal(true)}>
  1321. 访问接口
  1322. </Button>
  1323. </Space>
  1324. </Form>
  1325. </Card>
  1326. </Col>
  1327. </Row>
  1328. )}
  1329. {activeKey === "auth" && (
  1330. <Row gutter={[16, 16]}>
  1331. <Col xs={24} lg={12}>
  1332. <Card title="OAuth 2.0">
  1333. <Form
  1334. layout="vertical"
  1335. onFinish={() => globalMessage.success("已签发示例Token")}
  1336. initialValues={{ clientId: "demo-client", scope: "read:devices", grant: "client_credentials" }}
  1337. >
  1338. <Form.Item label="Client ID" name="clientId" rules={[{ required: true }]}>
  1339. <Input />
  1340. </Form.Item>
  1341. <Form.Item label="授权范围" name="scope">
  1342. <Input placeholder="read:devices write:commands" />
  1343. </Form.Item>
  1344. <Form.Item label="授权模式" name="grant">
  1345. <Select
  1346. options={[
  1347. { label: "Client Credentials", value: "client_credentials" },
  1348. { label: "Authorization Code", value: "authorization_code" },
  1349. ]}
  1350. />
  1351. </Form.Item>
  1352. <Button type="primary" htmlType="submit" icon={<ShieldCheck size={16} />}>
  1353. 获取Token
  1354. </Button>
  1355. </Form>
  1356. <Divider />
  1357. <Text>示例Token</Text>
  1358. <pre style={{ background: "#0b1020", color: "#d6e5ff", padding: 12, borderRadius: 8 }}>
  1359. {"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.demo.payload.signature"}
  1360. </pre>
  1361. </Card>
  1362. </Col>
  1363. <Col xs={24} lg={12}>
  1364. <Card title="RBAC 权限管理">
  1365. <Row gutter={12}>
  1366. <Col span={12}>
  1367. <Title level={5}>角色</Title>
  1368. <Table
  1369. size="small"
  1370. pagination={false}
  1371. dataSource={[
  1372. { key: "r1", name: "管理员", perms: "ALL" },
  1373. { key: "r2", name: "运维", perms: "devices:read, commands:write" },
  1374. { key: "r3", name: "访客", perms: "devices:read" },
  1375. ]}
  1376. columns={[
  1377. { title: "角色", dataIndex: "name" },
  1378. { title: "权限", dataIndex: "perms" },
  1379. ]}
  1380. />
  1381. </Col>
  1382. <Col span={12}>
  1383. <Title level={5}>用户</Title>
  1384. <Table
  1385. size="small"
  1386. pagination={false}
  1387. dataSource={[
  1388. { key: "u1", name: "alice", role: "管理员" },
  1389. { key: "u2", name: "bob", role: "运维" },
  1390. ]}
  1391. columns={[
  1392. { title: "用户", dataIndex: "name" },
  1393. { title: "角色", dataIndex: "role" },
  1394. {
  1395. title: "操作",
  1396. render: (_, r) => (
  1397. <Button size="small" onClick={() => setRbacModal({ open: true, user: r })}>
  1398. 分配权限
  1399. </Button>
  1400. ),
  1401. },
  1402. ]}
  1403. />
  1404. </Col>
  1405. </Row>
  1406. </Card>
  1407. </Col>
  1408. </Row>
  1409. )}
  1410. </Content>
  1411. </Layout>
  1412. {/* Drawer: 设备注册引导 */}
  1413. <Drawer title="设备注册与授权" open={registerDrawer} onClose={() => setRegisterDrawer(false)} width={640}>
  1414. <Steps
  1415. current={1}
  1416. items={[{ title: "选择产品" }, { title: "注册设备" }, { title: "获取密钥" }, { title: "接入测试" }]}
  1417. />
  1418. <Divider />
  1419. <Form
  1420. layout="vertical"
  1421. onFinish={(v) => {
  1422. addDevice({
  1423. id: v.id,
  1424. name: v.name,
  1425. projectId: products.find((p) => p.id === v.productId)?.projectId ?? projects[0]?.id,
  1426. productId: v.productId,
  1427. lat: 31.23 + Math.random() * 0.05,
  1428. lng: 121.47 + Math.random() * 0.05,
  1429. })
  1430. globalMessage.success("注册成功")
  1431. }}
  1432. >
  1433. <Form.Item label="产品" name="productId" rules={[{ required: true }]}>
  1434. <Select options={productOptions} />
  1435. </Form.Item>
  1436. <Row gutter={12}>
  1437. <Col span={12}>
  1438. <Form.Item label="设备ID" name="id" rules={[{ required: true }]}>
  1439. <Input placeholder="dev-xxx" />
  1440. </Form.Item>
  1441. </Col>
  1442. <Col span={12}>
  1443. <Form.Item label="设备名称" name="name" rules={[{ required: true }]}>
  1444. <Input />
  1445. </Form.Item>
  1446. </Col>
  1447. </Row>
  1448. <Form.Item>
  1449. <Space>
  1450. <Button htmlType="submit" type="primary" icon={<Check size={16} />}>
  1451. 注册
  1452. </Button>
  1453. <Button icon={<UploadIcon size={16} />} onClick={() => globalMessage.success("已接收CSV")}>
  1454. 导入CSV
  1455. </Button>
  1456. </Space>
  1457. </Form.Item>
  1458. </Form>
  1459. <Divider />
  1460. <Title level={5}>连接示例(MQTTS)</Title>
  1461. <pre style={{ background: "#0b1020", color: "#d6e5ff", padding: 12, borderRadius: 8 }}>
  1462. {`mqtts://iot.example.com:8883
  1463. username: <deviceId>
  1464. password: <deviceSecret>
  1465. topic: /up/<product>/<deviceId>`}
  1466. </pre>
  1467. </Drawer>
  1468. {/* Notifications modal */}
  1469. <Modal title="通知中心" open={notifOpen} onCancel={() => setNotifOpen(false)} onOk={() => setNotifOpen(false)}>
  1470. {alertLogs.length === 0 ? (
  1471. <Text type="secondary">暂无告警</Text>
  1472. ) : (
  1473. <Table
  1474. size="small"
  1475. pagination={false}
  1476. dataSource={alertLogs}
  1477. columns={[
  1478. { title: "时间", dataIndex: "time", width: 120 },
  1479. { title: "设备ID", dataIndex: "deviceId", width: 140 },
  1480. { title: "消息", dataIndex: "msg" },
  1481. ]}
  1482. />
  1483. )}
  1484. </Modal>
  1485. {/* OpenAPI config modal */}
  1486. <Modal
  1487. title={`服务配置 - ${svcModal.svc?.name ?? ""}`}
  1488. open={svcModal.open}
  1489. onCancel={() => setSvcModal({ open: false })}
  1490. onOk={() => {
  1491. setSvcModal({ open: false })
  1492. globalMessage.success("配置已保存")
  1493. }}
  1494. >
  1495. <Form layout="vertical" initialValues={svcModal.svc}>
  1496. <Form.Item label="名称" name="name">
  1497. <Input disabled />
  1498. </Form.Item>
  1499. <Form.Item label="路径" name="path">
  1500. <Input />
  1501. </Form.Item>
  1502. <Form.Item label="限流(req/s)" name="qps">
  1503. <Input placeholder="例如 200" />
  1504. </Form.Item>
  1505. </Form>
  1506. </Modal>
  1507. {/* Service weight modal */}
  1508. <Modal
  1509. title="实例权重"
  1510. open={weightModal.open}
  1511. onCancel={() => setWeightModal((s) => ({ ...s, open: false }))}
  1512. onOk={() => {
  1513. setWeightModal((s) => ({ ...s, open: false }))
  1514. globalMessage.success(`已设置权重:${weightModal.weight}`)
  1515. }}
  1516. >
  1517. <Slider
  1518. min={0}
  1519. max={100}
  1520. value={weightModal.weight}
  1521. onChange={(v) => setWeightModal({ open: true, weight: Number(v) })}
  1522. />
  1523. </Modal>
  1524. {/* Warehouse schedule modal */}
  1525. <Modal
  1526. title="ETL 计划配置"
  1527. open={scheduleModal}
  1528. onCancel={() => setScheduleModal(false)}
  1529. onOk={() => {
  1530. setScheduleModal(false)
  1531. globalMessage.success("计划已保存")
  1532. }}
  1533. >
  1534. <Form layout="vertical" initialValues={{ cron: "0 */1 * * *", retry: 3 }}>
  1535. <Form.Item label="Cron 表达式" name="cron">
  1536. <Input />
  1537. </Form.Item>
  1538. <Form.Item label="失败重试次数" name="retry">
  1539. <Input type="number" />
  1540. </Form.Item>
  1541. </Form>
  1542. </Modal>
  1543. {/* Config center API access modal */}
  1544. <Modal
  1545. title="配置访问接口"
  1546. open={apiAccessModal}
  1547. onCancel={() => setApiAccessModal(false)}
  1548. onOk={() => setApiAccessModal(false)}
  1549. >
  1550. <Descriptions size="small" column={1} bordered>
  1551. <Descriptions.Item label="GET">
  1552. <Text code>/config/{`{env}`}</Text>
  1553. </Descriptions.Item>
  1554. <Descriptions.Item label="POST 推送更新">
  1555. <Text code>/config/push</Text>
  1556. </Descriptions.Item>
  1557. </Descriptions>
  1558. </Modal>
  1559. {/* RBAC assign modal */}
  1560. <Modal
  1561. title={`分配权限 - ${rbacModal.user?.name ?? ""}`}
  1562. open={rbacModal.open}
  1563. onCancel={() => setRbacModal({ open: false })}
  1564. onOk={() => {
  1565. setRbacModal({ open: false })
  1566. globalMessage.success("权限已更新")
  1567. }}
  1568. >
  1569. <Form layout="vertical" initialValues={{ role: rbacModal.user?.role ?? "访客" }}>
  1570. <Form.Item label="角色" name="role">
  1571. <Select
  1572. options={[
  1573. { label: "管理员", value: "管理员" },
  1574. { label: "运维", value: "运维" },
  1575. { label: "访客", value: "访客" },
  1576. ]}
  1577. />
  1578. </Form.Item>
  1579. </Form>
  1580. </Modal>
  1581. </Layout>
  1582. )
  1583. }
  1584. function PlusIcon() {
  1585. return (
  1586. <span aria-hidden className="inline-flex">
  1587. <svg width="0" height="0" />
  1588. </span>
  1589. )
  1590. }
  1591. function AlertIcon() {
  1592. return (
  1593. <span aria-hidden className="inline-flex">
  1594. <svg width="0" height="0" />
  1595. </span>
  1596. )
  1597. }
  1598. function PlayIcon() {
  1599. return (
  1600. <span aria-hidden className="inline-flex">
  1601. <svg width="0" height="0" />
  1602. </span>
  1603. )
  1604. }