Home.vue 47 KB

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