Home.vue 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411
  1. <template>
  2. <div class="page" @mousemove="onMouseMove" @mouseleave="onMouseLeave">
  3. <!-- Top Header -->
  4. <header class="topbar">
  5. <div class="top-left">
  6. <div class="weather">
  7. <i class="dot-sun" aria-hidden="true"></i>
  8. <span class="w1">{{ header.weatherText }}</span>
  9. <span class="w2">{{ header.tempText }}</span>
  10. </div>
  11. </div>
  12. <div class="top-center" style="visibility: hidden;">
  13. <div class="title-frame">
  14. <div class="title">交通信号控制平台</div>
  15. </div>
  16. </div>
  17. <div class="top-right">
  18. <div class="clock">
  19. <div class="time">{{ header.timeText }}</div>
  20. <div class="date">
  21. <span>{{ header.weekText }}</span>
  22. <span class="sep">·</span>
  23. <span>{{ header.dateText }}</span>
  24. </div>
  25. </div>
  26. </div>
  27. </header>
  28. <!-- Main Layout -->
  29. <main class="grid">
  30. <!-- Left -->
  31. <section class="col left">
  32. <!-- 在线状态(2s循环:信号机/检测器/相机) -->
  33. <div class="card card-a">
  34. <div class="card-h">
  35. <span class="h-dot"></span>
  36. <span>在线状态</span>
  37. </div>
  38. <div class="tabs">
  39. <button
  40. v-for="t in onlineTabs"
  41. :key="t.key"
  42. class="tab"
  43. :class="{ active: onlineTab === t.key }"
  44. @click="setOnlineTab(t.key, true)"
  45. >
  46. {{ t.label }}
  47. </button>
  48. </div>
  49. <div class="row">
  50. <div ref="onlineChart" class="chart donut"></div>
  51. <div class="legend">
  52. <div class="lg">
  53. <span class="b b-on"></span><span>在线</span>
  54. <span class="num">{{ onlineView.online }}</span>
  55. </div>
  56. <div class="lg">
  57. <span class="b b-off"></span><span>离线</span>
  58. <span class="num">{{ onlineView.offline }}</span>
  59. </div>
  60. <div class="lg">
  61. <span class="b b-rate"></span><span>在线率</span>
  62. <span class="num">{{ onlineView.rate }}%</span>
  63. </div>
  64. <div class="subnote">
  65. {{ onlineTabsMap[onlineTab] }} · {{ onlineView.online }}/{{ onlineView.total }}
  66. </div>
  67. </div>
  68. <!-- <div class="dock-arrow right" @click="dockNext" title="下一页"></div> -->
  69. </div>
  70. </div>
  71. <!-- 控制模式(GB20999 / GAT1049) -->
  72. <div class="card card-b">
  73. <div class="card-h">
  74. <span class="h-dot"></span>
  75. <span>在线状态</span>
  76. </div>
  77. <div class="tabs small">
  78. <button class="tab" :class="{ active: true }">信号机</button>
  79. <button class="tab">检测器</button>
  80. <button class="tab">红绿灯</button>
  81. </div>
  82. <div class="row row-b">
  83. <div class="legend legend-b">
  84. <div class="lg" v-for="it in controlLegend" :key="it.k">
  85. <span class="b" :class="it.cls"></span><span>{{ it.t }}</span>
  86. <span class="num">{{ it.v }}</span>
  87. </div>
  88. </div>
  89. <div ref="controlChart" class="chart donut small"></div>
  90. </div>
  91. </div>
  92. <!-- 故障报警(展示4条,可忽略/查看,超过4条自动上移) -->
  93. <div class="card card-c grow">
  94. <div class="card-h">
  95. <span class="h-dot"></span>
  96. <span>故障报警</span>
  97. </div>
  98. <div class="alarm-list">
  99. <div class="alarm-item" v-for="a in alarmsView" :key="a.id">
  100. <div class="a-left">
  101. <div class="a-title">
  102. <span class="lvl" :class="'lv-' + a.level">{{ a.levelText }}</span>
  103. <span class="txt">{{ a.title }}</span>
  104. </div>
  105. <div class="a-sub">
  106. <span class="loc">{{ a.loc }}</span>
  107. <!-- <span class="time">{{ a.time }}</span> -->
  108. </div>
  109. </div>
  110. <div class="a-actions">
  111. <button class="btn ghost" @click="ignoreAlarm(a.id)">忽略</button>
  112. <button class="btn primary" @click="viewAlarm(a)">查看</button>
  113. </div>
  114. </div>
  115. <div v-if="alarmsView.length === 0" class="empty">暂无告警</div>
  116. </div>
  117. </div>
  118. </section>
  119. <!-- Middle -->
  120. <section class="col mid">
  121. <div class="map-wrap">
  122. <div class="map-frame">
  123. <!-- Top tools -->
  124. <div class="map-tools">
  125. <div class="search">
  126. <input v-model="mapQuery" placeholder="查询" />
  127. <button class="btn icon" @click="doSearch">查询</button>
  128. </div>
  129. <div class="select">
  130. <span class="lbl">全选</span>
  131. <span class="caret">▾</span>
  132. </div>
  133. </div>
  134. <!-- Map placeholder (可替换背景图) -->
  135. <div class="map-canvas" :style="mapBgStyle">
  136. <!-- roads overlay (placeholder) -->
  137. <svg class="roads" viewBox="0 0 1000 560" preserveAspectRatio="none" aria-hidden="true">
  138. <path class="r g" d="M80 410 C260 420, 380 360, 520 360 C660 360, 820 420, 940 420"/>
  139. <path class="r g" d="M120 150 C240 150, 310 190, 420 210 C520 230, 640 210, 820 150"/>
  140. <path class="r b" d="M520 60 L520 520"/>
  141. <path class="r g" d="M350 80 L350 520"/>
  142. <path class="r y" d="M170 330 L840 330"/>
  143. <path class="r red" d="M700 220 C720 260, 700 300, 680 330"/>
  144. <g class="nodes">
  145. <circle v-for="n in 10" :key="n" :cx="520" :cy="60 + n*42" r="6" class="node"/>
  146. </g>
  147. </svg>
  148. <!-- Popup (from alarm view) -->
  149. <div v-if="mapPopup" class="popup">
  150. <div class="popup-title">{{ mapPopup.title }}</div>
  151. <div class="popup-line">路口:{{ mapPopup.loc }}</div>
  152. <div class="popup-line">发生时间:{{ mapPopup.time }}</div>
  153. </div>
  154. <!-- Legend -->
  155. <div class="legend-box">
  156. <div class="legend-title">图例</div>
  157. <div class="legend-row" v-for="it in legendItems" :key="it.k">
  158. <span class="chip" :class="it.cls"></span>
  159. <span class="legend-t">{{ it.t }}</span>
  160. </div>
  161. </div>
  162. </div>
  163. <!-- Bottom Dock (overlay on map) -->
  164. <div class="dock-wrap" @mousemove="onDockMove" @mouseleave="onDockLeave">
  165. <div class="dock-arrow left" @click="dockPrev" title="上一页"></div>
  166. <div class="dockbar">
  167. <div
  168. v-for="(m, idx) in modules"
  169. :key="m.key"
  170. class="dock-item"
  171. :class="{ active: activeModule === m.key }"
  172. :style="navItemStyle(idx)"
  173. @click="selectModule(m)"
  174. >
  175. <div class="dock-icon" :style="navIconStyle(m)"></div>
  176. <div class="dock-label">{{ m.title }}</div>
  177. </div>
  178. </div>
  179. <div class="dock-arrow right" @click="dockNext" title="下一页"></div>
  180. </div>
  181. </div>
  182. </div>
  183. </section>
  184. <!-- Right -->
  185. <section class="col right">
  186. <!-- 设备状态(2s循环:信号机/检测器/红绿灯) -->
  187. <div class="card card-a">
  188. <div class="card-h">
  189. <span class="h-dot"></span>
  190. <span>设备状态</span>
  191. </div>
  192. <div class="tabs">
  193. <button
  194. v-for="t in deviceTabs"
  195. :key="t.key"
  196. class="tab"
  197. :class="{ active: deviceTab === t.key }"
  198. @click="setDeviceTab(t.key, true)"
  199. >
  200. {{ t.label }}
  201. </button>
  202. </div>
  203. <div class="row">
  204. <div ref="deviceChart" class="chart donut"></div>
  205. <div class="legend">
  206. <div class="lg">
  207. <span class="b b-ok"></span><span>正常</span>
  208. <span class="num">{{ deviceView.normal }}</span>
  209. </div>
  210. <div class="lg">
  211. <span class="b b-bad"></span><span>故障</span>
  212. <span class="num">{{ deviceView.fault }}</span>
  213. </div>
  214. <div class="subnote">
  215. {{ deviceTabsMap[deviceTab] }} · {{ deviceView.fault === 0 ? "全绿" : "需处理" }}
  216. </div>
  217. </div>
  218. <!-- <div class="dock-arrow right" @click="dockNext" title="下一页"></div> -->
  219. </div>
  220. </div>
  221. <!-- 勤务执行(>5滚动,点击可高亮地图) -->
  222. <div class="card card-c grow">
  223. <div class="card-h">
  224. <span class="h-dot"></span>
  225. <span>勤务执行</span>
  226. </div>
  227. <div class="tbl-wrap">
  228. <table class="tbl">
  229. <thead>
  230. <tr>
  231. <th style="width: 12%;">序号</th>
  232. <th style="width: 30%;">名称</th>
  233. <th style="width: 15%;">执行人</th>
  234. <th style="width: 15%;">等级</th>
  235. <th style="width: 15%;">状态</th>
  236. <th style="width: 13%;">操作</th>
  237. </tr>
  238. </thead>
  239. <tbody :style="dutyScrollStyle">
  240. <tr v-for="(r,i) in duty" :key="r.id" :class="{ hl: activeDutyId === r.id }">
  241. <td>{{ i + 1 }}</td>
  242. <td :title="r.name" class="name">{{ r.name }}</td>
  243. <td>{{ r.executor }}</td>
  244. <td><span class="tag" :class="'lv' + r.level">{{ r.levelText }}</span></td>
  245. <td><span class="st" :class="r.statusKey">{{ r.status }}</span></td>
  246. <td><a class="link" href="javascript:;" @click="goDuty(r)">查看</a></td>
  247. </tr>
  248. </tbody>
  249. </table>
  250. </div>
  251. </div>
  252. <!-- 关键路口(保留原平台设计界面:占位结构) -->
  253. <div class="card card-b">
  254. <div class="card-h">
  255. <span class="h-dot"></span>
  256. <span>关键路口</span>
  257. </div>
  258. <table class="tbl simple">
  259. <thead>
  260. <tr>
  261. <th>路口</th>
  262. <th style="width:98px;">运营模式</th>
  263. <th style="width:68px;">方案号</th>
  264. </tr>
  265. </thead>
  266. <tbody>
  267. <tr v-for="k in keyJunctions" :key="k.id">
  268. <td class="name">{{ k.name }}</td>
  269. <td>{{ k.mode }}</td>
  270. <td>{{ k.plan }}</td>
  271. </tr>
  272. </tbody>
  273. </table>
  274. </div>
  275. </section>
  276. </main>
  277. </div>
  278. </template>
  279. <script>
  280. import * as echarts from "echarts";
  281. export default {
  282. // eslint-disable-next-line vue/multi-word-component-names
  283. name: "Home",
  284. data() {
  285. return {
  286. baseW: 1920,
  287. baseH: 1080,
  288. scale: 1,
  289. mouseX: null,
  290. mouseY: null,
  291. activeDockIndex: 0,
  292. header: {
  293. weatherText: "晴",
  294. tempText: "32/17°C",
  295. timeText: "--:--:--",
  296. dateText: "--",
  297. weekText: "周五"
  298. },
  299. mapQuery: "",
  300. mapPopup: null,
  301. mapBgUrl: "", // 后期替换真实底图:在这里填 url
  302. legendItems: [
  303. { k: "center", t: "中心计划", cls: "c1" },
  304. { k: "coord", t: "干线协调", cls: "c2" },
  305. { k: "bus", t: "勤务路线", cls: "c3" },
  306. { k: "cycle", t: "定周期控制", cls: "c4" },
  307. { k: "sense", t: "感应控制", cls: "c5" },
  308. { k: "self", t: "自适应控制", cls: "c6" },
  309. { k: "manual", t: "手动控制", cls: "c7" },
  310. { k: "spec", t: "特殊控制", cls: "c8" },
  311. { k: "offline", t: "离线", cls: "c9" },
  312. { k: "drop", t: "降级", cls: "c10" },
  313. { k: "fault", t: "故障", cls: "c11" }
  314. ],
  315. onlineTab: "signal",
  316. onlineTabs: [
  317. { key: "signal", label: "信号机" },
  318. { key: "detector", label: "检测器" },
  319. { key: "camera", label: "相机" }
  320. ],
  321. onlineTabsMap: { signal: "信号机", detector: "检测器", camera: "相机" },
  322. onlineData: {
  323. signal: { online: 980, offline: 20, total: 1000, rate: 98 },
  324. detector: { online: 461, offline: 39, total: 500, rate: 92 },
  325. camera: { online: 298, offline: 12, total: 310, rate: 96 }
  326. },
  327. // 左侧第二块:控制信息(示例图的环形图)
  328. controlLegend: [
  329. { k: "cycle", t: "定周期控制", cls: "c4", v: 400 },
  330. { k: "sense", t: "感应控制", cls: "c5", v: 50 },
  331. { k: "coord", t: "干线协调", cls: "c2", v: 200 },
  332. { k: "spec", t: "黄闪控制", cls: "c8", v: 5 }
  333. ],
  334. controlTotal: 650,
  335. deviceTab: "signal",
  336. deviceTabs: [
  337. { key: "signal", label: "信号机" },
  338. { key: "detector", label: "检测器" },
  339. { key: "lamp", label: "红绿灯" }
  340. ],
  341. deviceTabsMap: { signal: "信号机", detector: "检测器", lamp: "红绿灯" },
  342. deviceData: {
  343. signal: { normal: 128, fault: 0 },
  344. detector: { normal: 89, fault: 2 },
  345. lamp: { normal: 312, fault: 4 }
  346. },
  347. alarms: [
  348. { id: "a1", level: 1, levelText: "一级", title: "通讯中断", loc: "中关村大街-科学院南路口-设备离线", time: "16:28:28" },
  349. { id: "a2", level: 2, levelText: "二级", title: "降级黄闪", loc: "中关村大街-科学院南路口-设备离线", time: "16:28:28" },
  350. { id: "a3", level: 3, levelText: "三级", title: "信号灯报警", loc: "知春路-学院路口-信号灯异常", time: "16:18:02" }
  351. ],
  352. duty: [
  353. { id: "d1", name: "大型活动交通安保", executor: "赵刚", level: 1, levelText: "一级", statusKey: "wait", status: "未开始" },
  354. { id: "d2", name: "道路施工路段交通引导", executor: "林小宇", level: 1, levelText: "一级", statusKey: "wait", status: "未开始" },
  355. { id: "d3", name: "酒驾醉驾专项查缉", executor: "周婷", level: 2, levelText: "二级", statusKey: "doing", status: "进行中" },
  356. { id: "d4", name: "交通信号灯故障排查", executor: "吴磊", level: 1, levelText: "一级", statusKey: "wait", status: "未开始" },
  357. { id: "d5", name: "应急救援通道清障", executor: "郑晓东", level: 1, levelText: "一级", statusKey: "wait", status: "未开始" }
  358. ],
  359. dutyScrollOffset: 0,
  360. activeDutyId: "",
  361. keyJunctions: [
  362. { id: "k1", name: "实行东街铁双园路交叉路口", mode: "定周期控制", plan: 4 },
  363. { id: "k2", name: "实行东街铁双园路交叉路口", mode: "自适应控制", plan: 1 },
  364. { id: "k3", name: "实行东街铁双园路交叉路口", mode: "感应控制", plan: 5 }
  365. ],
  366. modules: [
  367. { key: "home", title: "首页", img: "main-home.png", route: { path: "/home" } },
  368. { key: "watch", title: "状态监控", img: "main-watch.png", route: { path: "/home", query: { panel: "watch" } } },
  369. { key: "vip", title: "特勤安保", img: "main-security.png", route: { path: "/home", query: { panel: "security" } } },
  370. { key: "corr", title: "干线协调", img: "main-coor.png", route: { path: "/home", query: { panel: "coor" } } },
  371. { key: "overview", title: "状态展示", img: "main-surve.png", route: { path: "/home", query: { panel: "surve" } } },
  372. { key: "settings", title: "系统设置", img: "main-setting.png", route: { path: "/home", query: { panel: "setting" } } }
  373. ],
  374. activeModule: "home",
  375. hoverModule: "",
  376. onlineChart: null,
  377. controlChart: null,
  378. deviceChart: null,
  379. timerClock: null,
  380. timerCycle: null,
  381. timerDutyScroll: null,
  382. cycleIdx: 0,
  383. deviceCycleIdx: 0
  384. };
  385. },
  386. computed: {
  387. onlineView() {
  388. return this.onlineData[this.onlineTab] || { online: 0, offline: 0, total: 0, rate: 0 };
  389. },
  390. deviceView() {
  391. return this.deviceData[this.deviceTab] || { normal: 0, fault: 0 };
  392. },
  393. alarmsView() {
  394. const arr = [...this.alarms].sort((a, b) =>
  395. a.level !== b.level ? a.level - b.level : (b.time || "").localeCompare(a.time || "")
  396. );
  397. return arr.slice(0, 4);
  398. },
  399. mapBgStyle() {
  400. if (!this.mapBgUrl) return {};
  401. return { backgroundImage: `url(${this.mapBgUrl})` };
  402. },
  403. dutyScrollStyle() {
  404. if (this.duty.length <= 5) return {};
  405. return { transform: `translateY(${-this.dutyScrollOffset}px)` };
  406. }
  407. },
  408. mounted() {
  409. this.updateScale();
  410. window.addEventListener("resize", this.updateScale, { passive: true });
  411. this.startClock();
  412. this.initCharts();
  413. this.renderCharts();
  414. this.timerCycle = setInterval(() => {
  415. this.cycleIdx = (this.cycleIdx + 1) % this.onlineTabs.length;
  416. this.setOnlineTab(this.onlineTabs[this.cycleIdx].key, false);
  417. this.deviceCycleIdx = (this.deviceCycleIdx + 1) % this.deviceTabs.length;
  418. this.setDeviceTab(this.deviceTabs[this.deviceCycleIdx].key, false);
  419. }, 2000);
  420. this.timerDutyScroll = setInterval(() => {
  421. if (this.duty.length <= 5) return;
  422. this.dutyScrollOffset += 34;
  423. const max = (this.duty.length - 5) * 34;
  424. if (this.dutyScrollOffset > max) this.dutyScrollOffset = 0;
  425. }, 2200);
  426. window.addEventListener("resize", this.onResize, { passive: true });
  427. },
  428. beforeDestroy() {
  429. window.removeEventListener("resize", this.updateScale);
  430. window.removeEventListener("resize", this.onResize);
  431. clearInterval(this.timerClock);
  432. clearInterval(this.timerCycle);
  433. clearInterval(this.timerDutyScroll);
  434. this.onlineChart && this.onlineChart.dispose();
  435. this.controlChart && this.controlChart.dispose();
  436. this.deviceChart && this.deviceChart.dispose();
  437. },
  438. methods: {
  439. onMouseMove(e) {
  440. const rect = this.$el.getBoundingClientRect();
  441. this.mouseX = e.clientX - rect.left;
  442. this.mouseY = e.clientY - rect.top;
  443. },
  444. onMouseLeave() {
  445. this.mouseX = null;
  446. this.mouseY = null;
  447. },
  448. updateScale() {
  449. const w = window.innerWidth || this.baseW;
  450. const h = window.innerHeight || this.baseH;
  451. this.scale = Math.min(w / this.baseW, h / this.baseH);
  452. this.$el && this.$el.style.setProperty("--s", this.scale.toFixed(6));
  453. },
  454. onDockMove(e) {
  455. const rect = this.$el.getBoundingClientRect();
  456. this.mouseX = e.clientX - rect.left;
  457. this.mouseY = e.clientY - rect.top;
  458. },
  459. onDockLeave() {
  460. this.mouseX = null;
  461. this.mouseY = null;
  462. },
  463. navItemStyle(idx) {
  464. if (this.mouseX == null) return { transform: "translateZ(0) scale(1)" };
  465. const dock = this.$el.querySelector(".dockbar");
  466. const el = dock && dock.children && dock.children[idx];
  467. if (!el) return { transform: "translateZ(0) scale(1)" };
  468. const r = el.getBoundingClientRect();
  469. const root = this.$el.getBoundingClientRect();
  470. const cx = (r.left - root.left) + r.width / 2;
  471. const cy = (r.top - root.top) + r.height / 2;
  472. const dx = this.mouseX - cx;
  473. const dy = this.mouseY - cy;
  474. const dist = Math.sqrt(dx * dx + dy * dy);
  475. const R = 220 * this.scale;
  476. const t = Math.max(0, 1 - dist / R);
  477. const s = 1 + 0.35 * (t * t);
  478. return { transform: `translateZ(0) scale(${s.toFixed(4)})` };
  479. },
  480. onResize() {
  481. this.onlineChart && this.onlineChart.resize();
  482. this.controlChart && this.controlChart.resize();
  483. this.deviceChart && this.deviceChart.resize();
  484. },
  485. startClock() {
  486. const tick = () => {
  487. const d = new Date();
  488. const pad = (n) => String(n).padStart(2, "0");
  489. this.header.timeText = `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
  490. this.header.dateText = `${d.getFullYear()}.${pad(d.getMonth() + 1)}.${pad(d.getDate())}`;
  491. const wk = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"][d.getDay()];
  492. this.header.weekText = wk;
  493. };
  494. tick();
  495. this.timerClock = setInterval(tick, 1000);
  496. },
  497. initCharts() {
  498. this.onlineChart = echarts.init(this.$refs.onlineChart);
  499. this.controlChart = echarts.init(this.$refs.controlChart);
  500. this.deviceChart = echarts.init(this.$refs.deviceChart);
  501. },
  502. renderCharts() {
  503. this.renderOnlineChart();
  504. this.renderControlChart();
  505. this.renderDeviceChart();
  506. },
  507. renderOnlineChart() {
  508. const v = this.onlineView;
  509. this.onlineChart.setOption({
  510. animation: true,
  511. series: [
  512. {
  513. type: "pie",
  514. radius: ["70%", "88%"],
  515. center: ["50%", "50%"],
  516. silent: true,
  517. label: { show: false },
  518. labelLine: { show: false },
  519. data: [
  520. { value: v.online, name: "在线" },
  521. { value: Math.max(0, v.total - v.online), name: "离线" }
  522. ]
  523. }
  524. ],
  525. graphic: [
  526. {
  527. type: "text",
  528. left: "center",
  529. top: "40%",
  530. style: {
  531. text: `${v.rate}%`,
  532. fill: "rgba(235,255,255,0.92)",
  533. fontSize: 22,
  534. fontWeight: 800
  535. }
  536. },
  537. {
  538. type: "text",
  539. left: "center",
  540. top: "58%",
  541. style: {
  542. text: `${v.online}/${v.total}`,
  543. fill: "rgba(190,225,255,0.70)",
  544. fontSize: 12,
  545. fontWeight: 600
  546. }
  547. }
  548. ]
  549. });
  550. },
  551. renderControlChart() {
  552. const items = this.controlLegend || [];
  553. const total = this.controlTotal || items.reduce((s, x) => s + (x.v || 0), 0) || 0;
  554. this.controlChart.setOption({
  555. animation: true,
  556. series: [
  557. {
  558. type: "pie",
  559. radius: ["72%", "90%"],
  560. center: ["50%", "50%"],
  561. silent: true,
  562. label: { show: false },
  563. labelLine: { show: false },
  564. data: items.map((x) => ({ value: x.v, name: x.t }))
  565. }
  566. ],
  567. graphic: [
  568. {
  569. type: "text",
  570. left: "center",
  571. top: "40%",
  572. style: {
  573. text: `${total}个`,
  574. fill: "rgba(235,255,255,0.92)",
  575. fontSize: 18,
  576. fontWeight: 800
  577. }
  578. },
  579. {
  580. type: "text",
  581. left: "center",
  582. top: "58%",
  583. style: {
  584. text: "控制信息",
  585. fill: "rgba(190,225,255,0.70)",
  586. fontSize: 12,
  587. fontWeight: 600
  588. }
  589. }
  590. ]
  591. });
  592. },
  593. renderDeviceChart() {
  594. const v = this.deviceView;
  595. this.deviceChart.setOption({
  596. animation: true,
  597. series: [
  598. {
  599. type: "pie",
  600. radius: ["70%", "88%"],
  601. center: ["50%", "50%"],
  602. silent: true,
  603. label: { show: false },
  604. labelLine: { show: false },
  605. data: [
  606. { value: v.fault, name: "故障" },
  607. { value: Math.max(0, v.normal), name: "正常" }
  608. ]
  609. }
  610. ],
  611. graphic: [
  612. {
  613. type: "text",
  614. left: "center",
  615. top: "45%",
  616. style: {
  617. text: v.fault === 0 ? "故障 0" : `故障 ${v.fault}`,
  618. fill: "rgba(235,255,255,0.92)",
  619. fontSize: 18,
  620. fontWeight: 800
  621. }
  622. }
  623. ]
  624. });
  625. },
  626. setOnlineTab(key, byUser) {
  627. this.onlineTab = key;
  628. this.renderOnlineChart();
  629. if (byUser) this.cycleIdx = this.onlineTabs.findIndex((x) => x.key === key);
  630. },
  631. setDeviceTab(key, byUser) {
  632. this.deviceTab = key;
  633. this.renderDeviceChart();
  634. if (byUser) this.deviceCycleIdx = this.deviceTabs.findIndex((x) => x.key === key);
  635. },
  636. ignoreAlarm(id) {
  637. this.alarms = this.alarms.filter((x) => x.id !== id);
  638. },
  639. viewAlarm(a) {
  640. this.mapPopup = { title: a.title, loc: a.loc, time: a.time };
  641. setTimeout(() => {
  642. if (this.mapPopup && this.mapPopup.title === a.title) this.mapPopup = null;
  643. }, 3000);
  644. },
  645. goDuty(r) {
  646. this.activeDutyId = r.id;
  647. this.mapPopup = {
  648. title: "勤务任务提醒",
  649. loc: r.name,
  650. time: this.header.dateText + " " + this.header.timeText
  651. };
  652. },
  653. doSearch() {
  654. this.mapPopup = {
  655. title: "查询结果",
  656. loc: this.mapQuery || "(未输入)",
  657. time: this.header.dateText + " " + this.header.timeText
  658. };
  659. setTimeout(() => (this.mapPopup = null), 2200);
  660. },
  661. assetUrl(file) {
  662. try {
  663. return require("@/assets/main/" + file);
  664. } catch (e) {
  665. return "";
  666. }
  667. },
  668. navIconStyle(m) {
  669. return { backgroundImage: `url(${this.assetUrl(m.img)})` };
  670. },
  671. selectModule(m) {
  672. this.activeModule = m.key;
  673. if (m.route && this.$router) this.$router.push(m.route);
  674. },
  675. dockPrev() {
  676. if (typeof this.activeDockIndex !== "number") this.activeDockIndex = 0;
  677. const n = (this.dockItems && this.dockItems.length) ? this.dockItems.length : 6;
  678. this.activeDockIndex = (this.activeDockIndex - 1 + n) % n;
  679. },
  680. dockNext() {
  681. if (typeof this.activeDockIndex !== "number") this.activeDockIndex = 0;
  682. const n = (this.dockItems && this.dockItems.length) ? this.dockItems.length : 6;
  683. this.activeDockIndex = (this.activeDockIndex + 1) % n;
  684. },
  685. }
  686. };
  687. </script>
  688. <style scoped>
  689. /* ====== Root ====== */
  690. .page{
  691. width:100vw;
  692. height:100vh;
  693. overflow:hidden;
  694. background: radial-gradient(1200px 700px at 50% 25%, rgba(40,120,255,0.22), rgba(5,12,30,1) 58%);
  695. position:relative;
  696. --s: 1;
  697. font-family: "Microsoft YaHei", system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
  698. display:flex;
  699. flex-direction:column;
  700. }
  701. /* ====== Top ====== */
  702. .topbar{
  703. height: calc(var(--s) * 100px);
  704. display:grid;
  705. grid-template-columns: 1fr 1.6fr 1fr;
  706. align-items:center;
  707. padding: 0 calc(var(--s) * 26px);
  708. position:relative;
  709. background: center/100% 100% no-repeat;
  710. background-image: url("~@/assets/main/main-header.png");
  711. z-index: 10;
  712. }
  713. .topbar::before{ display:none; }
  714. .title-frame{
  715. width: min(62vw, calc(var(--s) * 980px));
  716. height: calc(var(--s) * 64px);
  717. border-radius: calc(var(--s) * 14px);
  718. background: rgba(10,35,70,0.35);
  719. box-shadow: inset 0 0 calc(var(--s) * 26px) rgba(0,160,255,0.18);
  720. display:flex;
  721. align-items:center;
  722. justify-content:center;
  723. }
  724. .title{
  725. font-size: calc(var(--s) * 34px);
  726. letter-spacing: calc(var(--s) * 2px);
  727. text-shadow: 0 0 calc(var(--s) * 10px) rgba(90, 200, 255, 0.35);
  728. font-weight: 600;
  729. }
  730. .clock{ text-align:right; color: rgba(235,255,255,0.92); }
  731. .time{ font-weight: 900; font-size: clamp(18px, 1.6vw, 28px); letter-spacing: 1px; }
  732. .date{ margin-top:4px; font-size: 12px; color: rgba(190,225,255,0.78); }
  733. .sep{ margin: 0 6px; opacity: 0.6; }
  734. /* ====== Grid ====== */
  735. .grid{
  736. flex: 1;
  737. min-height: 0;
  738. padding: clamp(10px, 1.6vw, 18px);
  739. display:grid;
  740. grid-template-columns: 1fr 2.1fr 1fr;
  741. gap: clamp(10px, 1.2vw, 16px);
  742. }
  743. .col{ min-width:0; min-height:0; }
  744. .col.left, .col.right{ display:grid; grid-template-rows: 1fr 1fr 1fr; gap: clamp(10px, 1.2vw, 16px); }
  745. .col.mid{ display:flex; flex-direction:column; gap: clamp(10px, 1.2vw, 16px); }
  746. .grow{ flex:1; min-height:0; overflow:hidden; }
  747. /* ====== Cards ====== */
  748. .card{
  749. display:flex;
  750. flex-direction:column;
  751. min-height:0;
  752. border-radius: 14px;
  753. background: linear-gradient(180deg, rgba(8,22,60,0.58), rgba(8,22,60,0.28));
  754. border: 1px solid rgba(80,200,255,0.18);
  755. box-shadow: 0 0 24px rgba(0,160,255,0.08);
  756. padding: 12px 12px;
  757. position:relative;
  758. overflow:hidden;
  759. }
  760. .card::before{
  761. content:"";
  762. position:absolute;
  763. inset:0;
  764. background:
  765. radial-gradient(500px 220px at 30% 0%, rgba(0,220,255,0.16), rgba(0,0,0,0)),
  766. radial-gradient(420px 220px at 80% 100%, rgba(30,120,255,0.10), rgba(0,0,0,0));
  767. pointer-events:none;
  768. }
  769. .card-h{
  770. position:relative;
  771. z-index:1;
  772. display:flex;
  773. align-items:center;
  774. gap:10px;
  775. color: rgba(230,250,255,0.92);
  776. font-weight: 800;
  777. letter-spacing: 0.5px;
  778. margin-bottom: 10px;
  779. }
  780. .h-dot{
  781. width: 6px; height: 6px; border-radius: 50%;
  782. background: rgba(0,240,255,0.9);
  783. box-shadow: 0 0 12px rgba(0,240,255,0.55);
  784. }
  785. /* Tabs */
  786. .tabs{
  787. position:relative; z-index:1;
  788. display:flex;
  789. gap: 10px;
  790. margin-bottom: 10px;
  791. }
  792. .tab{
  793. flex:1;
  794. height: 28px;
  795. border-radius: 8px;
  796. border: 1px solid rgba(80,200,255,0.18);
  797. background: rgba(10,30,80,0.28);
  798. color: rgba(190,225,255,0.82);
  799. cursor:pointer;
  800. font-weight:700;
  801. font-size: 12px;
  802. }
  803. .tabs.small{ margin-bottom: 8px; }
  804. .tabs.small .tab{ height: 26px; font-size: 12px; }
  805. .tab.active{
  806. background: rgba(0,180,255,0.18);
  807. color: rgba(235,255,255,0.92);
  808. box-shadow: 0 0 16px rgba(0,200,255,0.14);
  809. }
  810. /* Charts */
  811. .row{ position:relative; z-index:1; display:grid; grid-template-columns: 140px 1fr; gap: 12px; align-items:center; }
  812. .chart.donut{ width: 140px; height: 120px; }
  813. .chart.donut.small{ width: 120px; height: 108px; }
  814. .row.row-b{ grid-template-columns: 1fr 120px; }
  815. .legend.legend-b{ gap: 8px; }
  816. .legend.legend-b .lg{ grid-template-columns: 14px 1fr auto; }
  817. .legend{ display:flex; flex-direction:column; gap: 8px; }
  818. .lg{ display:flex; align-items:center; gap: 8px; color: rgba(200,235,255,0.78); font-size: 12px; }
  819. .lg .num{ margin-left:auto; color: rgba(235,255,255,0.92); font-weight: 800; }
  820. .subnote{ margin-top: 2px; color: rgba(170,215,255,0.62); font-size: 12px; }
  821. .b{ width:8px; height:8px; border-radius: 2px; display:inline-block; }
  822. .b-on{ background: rgba(80,240,255,0.95); }
  823. .b-off{ background: rgba(255,200,60,0.85); }
  824. .b-rate{ background: rgba(0,160,255,0.85); }
  825. .b-ok{ background: rgba(80,255,160,0.85); }
  826. .b-bad{ background: rgba(255,90,90,0.9); }
  827. /* Modes */
  828. .mode-grid{ position:relative; z-index:1; display:grid; gap: 10px; }
  829. .mode{
  830. border: 1px solid rgba(80,200,255,0.12);
  831. background: rgba(6,18,55,0.22);
  832. border-radius: 12px;
  833. padding: 10px;
  834. }
  835. .mode-h{ color: rgba(220,250,255,0.88); font-weight: 800; font-size: 12px; margin-bottom: 8px; }
  836. .mode-b{ display:flex; flex-wrap: wrap; gap: 8px; }
  837. .pill{
  838. padding: 6px 10px;
  839. border-radius: 999px;
  840. border: 1px solid rgba(80,200,255,0.16);
  841. background: rgba(10,30,80,0.28);
  842. color: rgba(190,225,255,0.82);
  843. font-size: 12px;
  844. font-weight: 700;
  845. }
  846. .pill.warn{ border-color: rgba(255,200,60,0.25); background: rgba(255,200,60,0.10); color: rgba(255,230,170,0.9); }
  847. .pill.mute{ border-color: rgba(160,180,220,0.16); background: rgba(10,30,80,0.12); color: rgba(170,200,240,0.75); }
  848. /* Alarms */
  849. .alarm-list{
  850. flex:1;
  851. min-height:0;
  852. overflow:auto;
  853. position:relative; z-index:1; height: 100%; overflow:hidden; padding-right: 6px; }
  854. .alarm-item{
  855. display:flex;
  856. justify-content:space-between;
  857. gap: 10px;
  858. padding: 10px 0;
  859. border-bottom: 1px solid rgba(80,200,255,0.10);
  860. }
  861. .a-title{ display:flex; align-items:center; gap: 10px; }
  862. .lvl{
  863. min-width: 38px;
  864. height: 18px;
  865. display:inline-flex;
  866. align-items:center;
  867. justify-content:center;
  868. border-radius: 6px;
  869. font-size: 11px;
  870. font-weight: 900;
  871. letter-spacing: 0.5px;
  872. }
  873. .lv-1{ background: rgba(255,90,90,0.16); border: 1px solid rgba(255,90,90,0.35); color: rgba(255,170,170,0.95); }
  874. .lv-2{ background: rgba(255,200,60,0.14); border: 1px solid rgba(255,200,60,0.30); color: rgba(255,235,190,0.92); }
  875. .lv-3{ background: rgba(0,200,255,0.12); border: 1px solid rgba(0,200,255,0.25); color: rgba(190,245,255,0.92); }
  876. .lv-4{ background: rgba(160,180,220,0.12); border: 1px solid rgba(160,180,220,0.22); color: rgba(205,225,255,0.85); }
  877. .txt{ color: rgba(235,255,255,0.92); font-weight: 900; }
  878. .a-sub{ margin-top: 6px; display:flex; gap: 10px; color: rgba(180,220,255,0.68); font-size: 12px; }
  879. .a-sub .time{ margin-left:auto; opacity:0.85; }
  880. .a-actions{ display:flex; align-items:center; gap: 8px; }
  881. .btn{
  882. height: 28px;
  883. padding: 0 10px;
  884. border-radius: 10px;
  885. border: 1px solid rgba(80,200,255,0.18);
  886. background: rgba(10,30,80,0.22);
  887. color: rgba(200,235,255,0.82);
  888. cursor:pointer;
  889. font-weight: 800;
  890. font-size: 12px;
  891. }
  892. .btn.primary{
  893. background: rgba(0,200,255,0.16);
  894. color: rgba(235,255,255,0.92);
  895. }
  896. .btn.ghost{ background: rgba(10,30,80,0.12); }
  897. .btn.icon{ height: 30px; border-radius: 10px; }
  898. .empty{ padding: 18px 0; color: rgba(180,220,255,0.55); text-align:center; }
  899. /* Map */
  900. .map-wrap{ flex:1; min-height:0; }
  901. .map-frame{
  902. position: relative;
  903. height:100%;
  904. border-radius: 16px;
  905. border: 1px solid rgba(80,200,255,0.18);
  906. background: linear-gradient(180deg, rgba(8,22,60,0.42), rgba(8,22,60,0.18));
  907. box-shadow: 0 0 28px rgba(0,160,255,0.08);
  908. position:relative;
  909. overflow:hidden;
  910. }
  911. .map-tools{
  912. position:absolute;
  913. top: 10px;
  914. left: 12px;
  915. right: 12px;
  916. display:flex;
  917. justify-content:space-between;
  918. gap: 10px;
  919. z-index: 3;
  920. }
  921. .search{
  922. display:flex;
  923. gap: 8px;
  924. align-items:center;
  925. }
  926. .search input{
  927. width: 180px;
  928. height: 30px;
  929. border-radius: 10px;
  930. border: 1px solid rgba(80,200,255,0.18);
  931. background: rgba(0,0,0,0.18);
  932. color: rgba(235,255,255,0.9);
  933. padding: 0 10px;
  934. outline:none;
  935. }
  936. .select{
  937. height: 30px;
  938. min-width: 100px;
  939. border-radius: 10px;
  940. border: 1px solid rgba(80,200,255,0.18);
  941. background: rgba(0,0,0,0.18);
  942. color: rgba(200,235,255,0.85);
  943. display:flex;
  944. align-items:center;
  945. justify-content:space-between;
  946. padding: 0 10px;
  947. }
  948. .caret{ opacity:0.8; }
  949. .map-canvas{
  950. padding-bottom: calc(var(--s) * 120px);
  951. position:absolute;
  952. inset:0;
  953. background-image:
  954. radial-gradient(900px 520px at 50% 35%, rgba(0,220,255,0.10), rgba(0,0,0,0)),
  955. radial-gradient(720px 420px at 30% 75%, rgba(30,120,255,0.10), rgba(0,0,0,0));
  956. background-size: cover;
  957. background-position:center;
  958. }
  959. .roads{
  960. position:absolute;
  961. inset:0;
  962. z-index: 1;
  963. opacity: 0.9;
  964. }
  965. .r{
  966. fill:none;
  967. stroke-width: 10;
  968. stroke-linecap: round;
  969. filter: drop-shadow(0 0 8px rgba(0,255,255,0.25));
  970. }
  971. .r.g{ stroke: rgba(60,255,160,0.82); }
  972. .r.b{ stroke: rgba(60,190,255,0.88); }
  973. .r.y{ stroke: rgba(255,220,80,0.88); }
  974. .r.red{ stroke: rgba(255,90,90,0.92); }
  975. .node{ fill: rgba(0,220,255,0.7); stroke: rgba(255,255,255,0.25); stroke-width: 2; }
  976. .popup{
  977. position:absolute;
  978. left: 50%;
  979. top: 62%;
  980. transform: translate(-50%, -50%);
  981. width: min(360px, 46vw);
  982. border-radius: 14px;
  983. border: 1px solid rgba(80,200,255,0.20);
  984. background: rgba(0,0,0,0.35);
  985. backdrop-filter: blur(10px);
  986. padding: 12px 14px;
  987. z-index: 2;
  988. }
  989. .popup-title{ color: rgba(235,255,255,0.92); font-weight: 900; margin-bottom: 6px; }
  990. .popup-line{ color: rgba(190,225,255,0.78); font-size: 12px; margin-top: 4px; }
  991. .legend-box{
  992. position:absolute;
  993. right: 14px;
  994. bottom: calc(var(--s) * 150px);
  995. width: 140px;
  996. border-radius: 14px;
  997. border: 1px solid rgba(80,200,255,0.16);
  998. background: rgba(0,0,0,0.22);
  999. backdrop-filter: blur(8px);
  1000. padding: 10px 10px;
  1001. z-index: 2;
  1002. }
  1003. .legend-title{ color: rgba(230,250,255,0.9); font-weight: 900; margin-bottom: 8px; }
  1004. .legend-row{ display:flex; align-items:center; gap: 8px; padding: 4px 0; color: rgba(190,225,255,0.78); font-size: 12px; }
  1005. .chip{ width:10px; height:10px; border-radius: 3px; display:inline-block; }
  1006. .c1{ background: rgba(60,190,255,0.88); }
  1007. .c2{ background: rgba(60,255,160,0.82); }
  1008. .c3{ background: rgba(255,220,80,0.88); }
  1009. .c4{ background: rgba(0,220,255,0.78); }
  1010. .c5{ background: rgba(170,255,240,0.78); }
  1011. .c6{ background: rgba(140,190,255,0.78); }
  1012. .c7{ background: rgba(255,180,120,0.78); }
  1013. .c8{ background: rgba(255,200,60,0.78); }
  1014. .c9{ background: rgba(120,140,180,0.78); }
  1015. .c10{ background: rgba(255,220,80,0.72); }
  1016. .c11{ background: rgba(255,90,90,0.90); }
  1017. /* Tables */
  1018. .tbl-wrap{
  1019. flex:1;
  1020. min-height:0;
  1021. overflow:auto;
  1022. height:100%; overflow:hidden; }
  1023. .tbl{
  1024. width:100%;
  1025. border-collapse: collapse;
  1026. color: rgba(220,250,255,0.86);
  1027. font-size: 12px;
  1028. table-layout: fixed;
  1029. }
  1030. .tbl thead th{
  1031. position:sticky;
  1032. top:0;
  1033. z-index:1;
  1034. background: rgba(8,22,60,0.65);
  1035. color: rgba(180,230,255,0.72);
  1036. font-weight: 900;
  1037. border-bottom: 1px solid rgba(80,200,255,0.12);
  1038. padding: 8px 6px;
  1039. text-align: center;
  1040. box-sizing: border-box;
  1041. }
  1042. .tbl td{
  1043. padding: 8px 6px;
  1044. border-bottom: 1px solid rgba(80,200,255,0.10);
  1045. text-align: center;
  1046. box-sizing: border-box;
  1047. }
  1048. .tbl .name{
  1049. color: rgba(235,255,255,0.92);
  1050. font-weight: 800;
  1051. white-space: nowrap;
  1052. overflow: hidden;
  1053. text-overflow: ellipsis;
  1054. cursor: default;
  1055. }
  1056. .tbl tbody tr.hl{ background: rgba(0,200,255,0.10); }
  1057. .tag{
  1058. display:inline-flex;
  1059. align-items:center;
  1060. justify-content:center;
  1061. height: 18px;
  1062. min-width: 40px;
  1063. border-radius: 6px;
  1064. font-weight: 900;
  1065. font-size: 11px;
  1066. border: 1px solid rgba(80,200,255,0.14);
  1067. color: rgba(190,245,255,0.9);
  1068. background: rgba(0,200,255,0.10);
  1069. }
  1070. .tag.lv1{ border-color: rgba(0,220,255,0.20); }
  1071. .tag.lv2{ border-color: rgba(255,220,80,0.25); color: rgba(255,240,200,0.92); background: rgba(255,220,80,0.10); }
  1072. .tag.lv3{ border-color: rgba(255,140,140,0.24); color: rgba(255,200,200,0.92); background: rgba(255,90,90,0.10); }
  1073. .tag.lv4{ border-color: rgba(160,180,220,0.18); color: rgba(210,230,255,0.86); background: rgba(160,180,220,0.10); }
  1074. .st{ font-weight: 900; }
  1075. .st.wait{ color: rgba(190,225,255,0.78); }
  1076. .st.run{ color: rgba(255,220,80,0.92); }
  1077. .link{
  1078. color: rgba(130,230,255,0.95);
  1079. text-decoration:none;
  1080. font-weight: 900;
  1081. }
  1082. .tbl.simple thead th{ position:static; background: transparent; }
  1083. /* ====== Bottom nav ====== */
  1084. .bottombar{
  1085. flex: 0 0 auto;
  1086. height: clamp(88px, 11vh, 122px);
  1087. display:flex;
  1088. align-items:center;
  1089. justify-content:center;
  1090. padding: 0 14px 10px;
  1091. }
  1092. .nav-row{
  1093. display:flex;
  1094. align-items:center;
  1095. justify-content:center;
  1096. gap: clamp(10px, 1vw, 16px);
  1097. }
  1098. .bottombar::before{
  1099. content:"";
  1100. position:absolute;
  1101. inset:0;
  1102. background: linear-gradient(0deg, rgba(10,30,80,0.85), rgba(10,30,80,0.20));
  1103. border-top: 1px solid rgba(80,200,255,0.16);
  1104. pointer-events:none;
  1105. }
  1106. .nav-viewport{
  1107. width: min(860px, 78vw);
  1108. overflow:hidden;
  1109. position:relative;
  1110. z-index: 1;
  1111. }
  1112. .nav-track{
  1113. display:flex;
  1114. gap: 12px;
  1115. transition: transform 0.25s ease;
  1116. will-change: transform;
  1117. }
  1118. .nav-item{
  1119. width: clamp(92px, 6.8vw, 124px);
  1120. height: clamp(72px, 9vh, 96px);
  1121. border-radius: 16px;
  1122. border: 1px solid rgba(80,200,255,0.16);
  1123. background: rgba(0,0,0,0.12);
  1124. cursor:pointer;
  1125. display:flex;
  1126. flex-direction:column;
  1127. align-items:center;
  1128. justify-content:center;
  1129. gap: 8px;
  1130. color: rgba(200,235,255,0.76);
  1131. position:relative;
  1132. transition: transform .16s ease, border-color .16s ease, box-shadow .16s ease;
  1133. }
  1134. .nav-item.active{
  1135. background: rgba(0,200,255,0.10);
  1136. color: rgba(235,255,255,0.92);
  1137. box-shadow: 0 0 20px rgba(0,200,255,0.14);
  1138. }
  1139. .nav-item:hover{
  1140. transform: translateZ(0) scale(1.03);
  1141. border-color: rgba(0,220,255,0.26);
  1142. box-shadow: 0 0 20px rgba(0,220,255,0.10);
  1143. }
  1144. .nav-ic{
  1145. width: clamp(58px, 4.8vw, 86px);
  1146. height: clamp(52px, 5.8vh, 78px);
  1147. border-radius: 14px;
  1148. display:flex;
  1149. align-items:center;
  1150. justify-content:center;
  1151. border: 1px solid rgba(80,200,255,0.18);
  1152. background: rgba(10,30,80,0.22);
  1153. }
  1154. .nav-img{
  1155. width: 100%;
  1156. background: center/contain no-repeat;
  1157. filter: drop-shadow(0 0 10px rgba(80,200,255,0.35));
  1158. }
  1159. .nav-ic.glow{
  1160. box-shadow: 0 0 18px rgba(0,220,255,0.22);
  1161. background: rgba(0,200,255,0.10);
  1162. }
  1163. .nav-t{ font-weight: 900; font-size: 12px; }
  1164. .arrow{
  1165. position:relative;
  1166. z-index: 2;
  1167. width: 38px; height: 38px;
  1168. border-radius: 999px;
  1169. border: 1px solid rgba(80,200,255,0.18);
  1170. background: rgba(0,0,0,0.18);
  1171. color: rgba(220,250,255,0.9);
  1172. font-size: 22px;
  1173. line-height: 38px;
  1174. cursor:pointer;
  1175. }
  1176. /* Icons (CSS placeholders) */
  1177. .ic{ width:18px; height:18px; display:inline-block; position:relative; }
  1178. .ic-home::before{ content:""; position:absolute; inset:2px 3px 6px 3px; border:2px solid rgba(190,245,255,0.85); border-bottom:none; transform: skewY(-10deg); }
  1179. .ic-home::after{ content:""; position:absolute; left:5px; right:5px; bottom:6px; height:10px; border:2px solid rgba(190,245,255,0.85); background: rgba(0,200,255,0.06); }
  1180. .ic-eye::before{ content:""; position:absolute; left:1px; right:1px; top:5px; height:10px; border:2px solid rgba(190,245,255,0.85); border-radius: 999px; }
  1181. .ic-eye::after{ content:""; position:absolute; left:7px; top:9px; width:4px; height:4px; border-radius: 999px; background: rgba(190,245,255,0.85); }
  1182. .ic-shield::before{ content:""; position:absolute; left:4px; right:4px; top:2px; bottom:2px; border:2px solid rgba(190,245,255,0.85); border-radius: 8px; clip-path: polygon(0 0, 100% 0, 100% 55%, 50% 100%, 0 55%); }
  1183. .ic-road::before{ content:""; position:absolute; left:7px; top:2px; bottom:2px; width:4px; border-left:2px solid rgba(190,245,255,0.85); border-right:2px solid rgba(190,245,255,0.85); }
  1184. .ic-road::after{ content:""; position:absolute; left:9px; top:4px; bottom:4px; width:0; border-left:2px dashed rgba(190,245,255,0.65); }
  1185. .ic-chart::before{ content:""; position:absolute; left:2px; right:2px; bottom:2px; top:2px; border:2px solid rgba(190,245,255,0.85); border-radius: 6px; }
  1186. .ic-chart::after{ content:""; position:absolute; left:5px; bottom:6px; width:10px; height:6px; border-left:2px solid rgba(0,220,255,0.85); border-bottom:2px solid rgba(0,220,255,0.85); transform: skewX(-15deg); }
  1187. .ic-gear::before{ content:""; position:absolute; inset:3px; border:2px solid rgba(190,245,255,0.85); border-radius: 999px; }
  1188. .ic-gear::after{ content:""; position:absolute; inset:7px; background: rgba(190,245,255,0.85); border-radius: 999px; opacity:0.8; }
  1189. .ic-bell::before{ content:""; position:absolute; left:4px; right:4px; top:3px; bottom:4px; border:2px solid rgba(190,245,255,0.85); border-radius: 10px 10px 8px 8px; }
  1190. .ic-bell::after{ content:""; position:absolute; left:8px; bottom:2px; width:4px; height:4px; border-radius: 999px; background: rgba(190,245,255,0.85); }
  1191. .ic-chip::before{ content:""; position:absolute; inset:3px; border:2px solid rgba(190,245,255,0.85); border-radius: 6px; }
  1192. .ic-chip::after{ content:""; position:absolute; left:6px; right:6px; top:8px; height:0; border-top:2px dashed rgba(190,245,255,0.65); }
  1193. /* Responsive */
  1194. @media (max-width: 1180px){
  1195. .grid{ grid-template-columns: 1fr; }
  1196. .nav-viewport{ width: 94vw; }
  1197. .map-frame{ min-height: 42vh; }
  1198. }
  1199. /* ====== Chart layout align with UI sample ====== */
  1200. .card-a .row{ flex-direction: row-reverse; gap: calc(var(--s) * 14px); }
  1201. .card-a .donut{ width: calc(var(--s) * 160px); height: calc(var(--s) * 160px); }
  1202. .card-a .legend{ flex: 1; padding-right: calc(var(--s) * 6px); }
  1203. .card-b .row-b{ flex-direction: row; gap: calc(var(--s) * 14px); }
  1204. .card-b .donut.small{ width: calc(var(--s) * 150px); height: calc(var(--s) * 150px); }
  1205. .card-r1 .row{ flex-direction: row-reverse; gap: calc(var(--s) * 14px); }
  1206. .card-r1 .donut{ width: calc(var(--s) * 160px); height: calc(var(--s) * 160px); }
  1207. /* ====== Bottom Dock (overlay on map) ====== */
  1208. .dock-wrap{
  1209. position:absolute;
  1210. left:50%;
  1211. bottom: calc(var(--s) * 14px);
  1212. transform: translateX(-50%);
  1213. width: min(86%, calc(var(--s) * 980px)); /* 基本与地图主体宽度一致 */
  1214. display:flex;
  1215. justify-content:center;
  1216. pointer-events:auto;
  1217. z-index: 9;
  1218. }
  1219. .dockbar{
  1220. width: 100%;
  1221. display:flex;
  1222. align-items:flex-end;
  1223. justify-content:space-between;
  1224. gap: calc(var(--s) * 22px);
  1225. padding: calc(var(--s) * 6px) calc(var(--s) * 10px);
  1226. }
  1227. .dock-item{
  1228. width: calc(var(--s) * 98px);
  1229. height: calc(var(--s) * 124px);
  1230. display:flex;
  1231. flex-direction:column;
  1232. align-items:center;
  1233. justify-content:flex-start;
  1234. cursor:pointer;
  1235. transform-origin: 50% 80%;
  1236. transition: transform .08s linear;
  1237. }
  1238. .dock-icon{
  1239. width: calc(var(--s) * 98px);
  1240. height: calc(var(--s) * 86px);
  1241. background: center/contain no-repeat;
  1242. filter: drop-shadow(0 0 calc(var(--s) * 12px) rgba(70, 190, 255, .35));
  1243. }
  1244. .dock-label{
  1245. margin-top: calc(var(--s) * 10px);
  1246. font-size: calc(var(--s) * 14px);
  1247. line-height: 1;
  1248. opacity: .92;
  1249. text-shadow: 0 0 calc(var(--s) * 8px) rgba(0, 160, 255, .25);
  1250. color: rgba(235,255,255,0.92);
  1251. text-shadow: 0 0 calc(var(--s) * 10px) rgba(0,160,255,.35);
  1252. }
  1253. .dock-item:hover .dock-icon{
  1254. filter: drop-shadow(0 0 calc(var(--s) * 18px) rgba(120, 230, 255, .65));
  1255. }
  1256. /* ====== Patch: header corner info (match 示例图) ====== */
  1257. .top-left, .top-right{
  1258. align-self: start;
  1259. padding-top: calc(var(--s) * 60px);
  1260. }
  1261. .weather{
  1262. display:flex;
  1263. align-items:center;
  1264. gap: calc(var(--s) * 10px);
  1265. color: rgba(235,255,255,0.92);
  1266. }
  1267. .dot-sun{
  1268. width: calc(var(--s) * 8px);
  1269. height: calc(var(--s) * 8px);
  1270. border-radius: 50%;
  1271. background: rgba(255,220,120,0.95);
  1272. box-shadow: 0 0 calc(var(--s) * 10px) rgba(255,220,120,0.55);
  1273. display:inline-block;
  1274. }
  1275. .w1{ font-size: calc(var(--s) * 18px); font-weight: 700; letter-spacing: 1px; }
  1276. .w2{ font-size: calc(var(--s) * 16px); opacity: .9; }
  1277. .clock{ text-align:right; color: rgba(235,255,255,0.92); }
  1278. .time{
  1279. font-weight: 900;
  1280. font-size: calc(var(--s) * 28px);
  1281. line-height: 1;
  1282. letter-spacing: 1px;
  1283. text-shadow: 0 0 calc(var(--s) * 10px) rgba(90, 200, 255, 0.28);
  1284. }
  1285. .date{
  1286. margin-top: calc(var(--s) * 6px);
  1287. font-size: calc(var(--s) * 12px);
  1288. color: rgba(190,225,255,0.78);
  1289. }
  1290. /* ====== Patch: side cards sizing (avoid squashed/hidden) ====== */
  1291. .left .card-a, .right .card-a{ flex: 0 0 calc(var(--s) * 220px); }
  1292. .left .card-b{ flex: 0 0 calc(var(--s) * 240px); }
  1293. .right .card-b{ flex: 0 0 calc(var(--s) * 230px); }
  1294. .left .grow, .right .grow{ flex: 1 1 auto; min-height: calc(var(--s) * 230px); }
  1295. /* ====== Patch: dock arrows + label color (match 示例图) ====== */
  1296. .dock-wrap{
  1297. display:flex;
  1298. align-items:center;
  1299. justify-content:center;
  1300. gap: calc(var(--s) * 22px);
  1301. }
  1302. .dock-arrow{
  1303. width: calc(var(--s) * 54px);
  1304. height: calc(var(--s) * 54px);
  1305. background: center/contain no-repeat;
  1306. opacity: .92;
  1307. cursor: pointer;
  1308. transition: transform .18s ease, opacity .18s ease, filter .18s ease;
  1309. filter: drop-shadow(0 0 calc(var(--s) * 10px) rgba(60, 180, 255, .35));
  1310. }
  1311. .dock-arrow:hover{
  1312. opacity: 1;
  1313. transform: translateZ(0) scale(1.08);
  1314. filter: drop-shadow(0 0 calc(var(--s) * 14px) rgba(90, 210, 255, .6));
  1315. }
  1316. .dock-arrow.left{ background-image: url("~@/assets/main/main-left.png"); }
  1317. .dock-arrow.right{ background-image: url("~@/assets/main/main-right.png"); }
  1318. .dock-label{
  1319. color: rgba(235,255,255,0.92) !important;
  1320. text-shadow: 0 0 calc(var(--s) * 8px) rgba(0, 160, 255, 0.28);
  1321. }
  1322. </style>