wzz vor 3 Monaten
Ursprung
Commit
7783466628

BIN
src/assets/main/main-bg.png


BIN
src/assets/main/main-coor.png


BIN
src/assets/main/main-header.png


BIN
src/assets/main/main-home.png


BIN
src/assets/main/main-left.png


BIN
src/assets/main/main-right.png


BIN
src/assets/main/main-security.png


BIN
src/assets/main/main-setting.png


BIN
src/assets/main/main-surve.png


BIN
src/assets/main/main-watch.png


+ 2 - 0
src/router/index.js

@@ -4,6 +4,7 @@ import Router from "vue-router";
 import Login from "@/views/Login.vue";
 import Transition from "@/views/Transition.vue";
 import Home from "@/views/Home.vue";
+import Main from "@/views/Main.vue";
 
 Vue.use(Router);
 
@@ -12,6 +13,7 @@ export default new Router({
   routes: [
     { path: "/", redirect: "/login" },
     { path: "/login", component: Login },
+    { path: "/main", component: Main },
     { path: "/transition", component: Transition },
     { path: "/home", component: Home }
   ]

+ 389 - 143
src/views/Home.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="page">
+  <div class="page" @mousemove="onMouseMove" @mouseleave="onMouseLeave">
     <!-- Top Header -->
     <header class="topbar">
       <div class="top-left">
@@ -10,7 +10,7 @@
         </div>
       </div>
 
-      <div class="top-center">
+      <div class="top-center" style="visibility: hidden;">
         <div class="title-frame">
           <div class="title">交通信号控制平台</div>
         </div>
@@ -71,6 +71,7 @@
                 {{ onlineTabsMap[onlineTab] }} · {{ onlineView.online }}/{{ onlineView.total }}
               </div>
             </div>
+            <!-- <div class="dock-arrow right" @click="dockNext" title="下一页"></div> -->
           </div>
         </div>
 
@@ -78,36 +79,24 @@
         <div class="card card-b">
           <div class="card-h">
             <span class="h-dot"></span>
-            <span>控制模式</span>
+            <span>在线状态</span>
           </div>
 
-          <div class="mode-grid">
-            <div class="mode">
-              <div class="mode-h">中心控制</div>
-              <div class="mode-b">
-                <span class="pill">中心控制</span>
-              </div>
-            </div>
+          <div class="tabs small">
+            <button class="tab" :class="{ active: true }">信号机</button>
+            <button class="tab">检测器</button>
+            <button class="tab">红绿灯</button>
+          </div>
 
-            <div class="mode">
-              <div class="mode-h">本地控制</div>
-              <div class="mode-b">
-                <span class="pill">定周期</span>
-                <span class="pill">感应</span>
-                <span class="pill">协调</span>
-                <span class="pill">自适应</span>
-                <span class="pill">手动</span>
+          <div class="row row-b">
+            <div class="legend legend-b">
+              <div class="lg" v-for="it in controlLegend" :key="it.k">
+                <span class="b" :class="it.cls"></span><span>{{ it.t }}</span>
+                <span class="num">{{ it.v }}</span>
               </div>
             </div>
 
-            <div class="mode">
-              <div class="mode-h">特殊控制</div>
-              <div class="mode-b">
-                <span class="pill warn">黄闪</span>
-                <span class="pill warn">全红</span>
-                <span class="pill mute">关灯</span>
-              </div>
-            </div>
+            <div ref="controlChart" class="chart donut small"></div>
           </div>
         </div>
 
@@ -127,7 +116,7 @@
                 </div>
                 <div class="a-sub">
                   <span class="loc">{{ a.loc }}</span>
-                  <span class="time">{{ a.time }}</span>
+                  <!-- <span class="time">{{ a.time }}</span> -->
                 </div>
               </div>
               <div class="a-actions">
@@ -188,6 +177,25 @@
                 </div>
               </div>
             </div>
+
+          <!-- Bottom Dock (overlay on map) -->
+          <div class="dock-wrap" @mousemove="onDockMove" @mouseleave="onDockLeave">
+            <div class="dock-arrow left" @click="dockPrev" title="上一页"></div>
+            <div class="dockbar">
+              <div
+                v-for="(m, idx) in modules"
+                :key="m.key"
+                class="dock-item"
+                :class="{ active: activeModule === m.key }"
+                :style="navItemStyle(idx)"
+                @click="selectModule(m)"
+              >
+                <div class="dock-icon" :style="navIconStyle(m)"></div>
+                <div class="dock-label">{{ m.title }}</div>
+              </div>
+            </div>
+            <div class="dock-arrow right" @click="dockNext" title="下一页"></div>
+          </div>
           </div>
         </div>
       </section>
@@ -228,6 +236,7 @@
                 {{ deviceTabsMap[deviceTab] }} · {{ deviceView.fault === 0 ? "全绿" : "需处理" }}
               </div>
             </div>
+            <!-- <div class="dock-arrow right" @click="dockNext" title="下一页"></div> -->
           </div>
         </div>
 
@@ -289,32 +298,6 @@
         </div>
       </section>
     </main>
-
-    <!-- Bottom Nav (模块一级菜单,>6可滑动) -->
-    <footer class="bottombar">
-      <button class="arrow" v-if="modules.length > 6" @click="scrollModules(-1)" aria-label="left">‹</button>
-
-      <div class="nav-viewport">
-        <div class="nav-track" :style="navTrackStyle">
-          <button
-            v-for="m in modules"
-            :key="m.key"
-            class="nav-item"
-            :class="{ active: activeModule === m.key }"
-            @click="selectModule(m)"
-            @mouseenter="hoverModule = m.key"
-            @mouseleave="hoverModule = ''"
-          >
-            <div class="nav-ic" :class="{ glow: hoverModule === m.key || activeModule === m.key }">
-              <span class="ic" :class="m.icon"></span>
-            </div>
-            <div class="nav-t">{{ m.title }}</div>
-          </button>
-        </div>
-      </div>
-
-      <button class="arrow" v-if="modules.length > 6" @click="scrollModules(1)" aria-label="right">›</button>
-    </footer>
   </div>
 </template>
 
@@ -322,9 +305,16 @@
 import * as echarts from "echarts";
 
 export default {
+  // eslint-disable-next-line vue/multi-word-component-names
   name: "Home",
   data() {
     return {
+      baseW: 1920,
+      baseH: 1080,
+      scale: 1,
+      mouseX: null,
+      mouseY: null,
+      activeDockIndex: 0,
       header: {
         weatherText: "晴",
         tempText: "32/17°C",
@@ -364,7 +354,16 @@ export default {
         camera: { online: 298, offline: 12, total: 310, rate: 96 }
       },
 
-      deviceTab: "signal",
+      
+      // 左侧第二块:控制信息(示例图的环形图)
+      controlLegend: [
+        { k: "cycle", t: "定周期控制", cls: "c4", v: 400 },
+        { k: "sense", t: "感应控制", cls: "c5", v: 50 },
+        { k: "coord", t: "干线协调", cls: "c2", v: 200 },
+        { k: "spec", t: "黄闪控制", cls: "c8", v: 5 }
+      ],
+      controlTotal: 650,
+deviceTab: "signal",
       deviceTabs: [
         { key: "signal", label: "信号机" },
         { key: "detector", label: "检测器" },
@@ -380,18 +379,13 @@ export default {
       alarms: [
         { id: "a1", level: 1, levelText: "一级", title: "通讯中断", loc: "中关村大街-科学院南路口-设备离线", time: "16:28:28" },
         { id: "a2", level: 2, levelText: "二级", title: "降级黄闪", loc: "中关村大街-科学院南路口-设备离线", time: "16:28:28" },
-        { id: "a3", level: 3, levelText: "三级", title: "降级黄闪", loc: "中关村大街-科学院南路口-设备离线", time: "16:28:28" },
-        { id: "a4", level: 4, levelText: "四级", title: "检测器报警", loc: "海淀路-彩和坊路口-检测器异常", time: "16:22:10" },
-        { id: "a5", level: 2, levelText: "二级", title: "信号灯报警", loc: "知春路-学院路口-信号灯异常", time: "16:18:02" }
-      ],
-
-      duty: [
+        { id: "a3", level: 3, levelText: "三级", title: "信号灯报警", loc: "知春路-学院路口-信号灯异常", time: "16:18:02" }
+      ],      duty: [
         { id: "d1", name: "测试", executor: "测试", level: 1, levelText: "一级", statusKey: "wait", status: "未开始" },
         { id: "d2", name: "张飞", executor: "张飞", level: 1, levelText: "一级", statusKey: "wait", status: "未开始" },
-        { id: "d3", name: "关将", executor: "关将", level: 2, levelText: "二级", statusKey: "run", status: "进行中" },
+        { id: "d3", name: "关将", executor: "关将", level: 2, levelText: "二级", statusKey: "doing", status: "进行中" },
         { id: "d4", name: "刘备", executor: "刘备", level: 1, levelText: "一级", statusKey: "wait", status: "未开始" },
-        { id: "d5", name: "孙权", executor: "孙权", level: 1, levelText: "一级", statusKey: "wait", status: "未开始" },
-        { id: "d6", name: "赵云", executor: "赵云", level: 3, levelText: "三级", statusKey: "wait", status: "未开始" }
+        { id: "d5", name: "孙权", executor: "孙权", level: 1, levelText: "一级", statusKey: "wait", status: "未开始" }
       ],
       dutyScrollOffset: 0,
       activeDutyId: "",
@@ -403,20 +397,17 @@ export default {
       ],
 
       modules: [
-        { key: "home", title: "首页", icon: "ic-home", route: "/home" },
-        { key: "status", title: "状态监控", icon: "ic-eye", route: "/status" },
-        { key: "vip", title: "特勤安保", icon: "ic-shield", route: "/vip" },
-        { key: "corr", title: "干线协调", icon: "ic-road", route: "/corridor" },
-        { key: "overview", title: "状态展示", icon: "ic-chart", route: "/overview" },
-        { key: "settings", title: "系统设置", icon: "ic-gear", route: "/settings" },
-        { key: "fault", title: "故障报警", icon: "ic-bell", route: "/fault" },
-        { key: "device", title: "设备状态", icon: "ic-chip", route: "/device" }
+        { key: "home", title: "首页", img: "main-home.png", route: { path: "/home" } },
+        { key: "watch", title: "状态监控", img: "main-watch.png", route: { path: "/home", query: { panel: "watch" } } },
+        { key: "vip", title: "特勤安保", img: "main-security.png", route: { path: "/home", query: { panel: "security" } } },
+        { key: "corr", title: "干线协调", img: "main-coor.png", route: { path: "/home", query: { panel: "coor" } } },
+        { key: "overview", title: "状态展示", img: "main-surve.png", route: { path: "/home", query: { panel: "surve" } } },
+        { key: "settings", title: "系统设置", img: "main-setting.png", route: { path: "/home", query: { panel: "setting" } } }
       ],
       activeModule: "home",
       hoverModule: "",
-      navIndex: 0,
-
-      onlineChart: null,
+onlineChart: null,
+      controlChart: null,
       deviceChart: null,
 
       timerClock: null,
@@ -439,11 +430,7 @@ export default {
       );
       return arr.slice(0, 4);
     },
-    navTrackStyle() {
-      const step = 124;
-      const x = -this.navIndex * step;
-      return { transform: `translateX(${x}px)` };
-    },
+
     mapBgStyle() {
       if (!this.mapBgUrl) return {};
       return { backgroundImage: `url(${this.mapBgUrl})` };
@@ -454,6 +441,8 @@ export default {
     }
   },
   mounted() {
+    this.updateScale();
+    window.addEventListener("resize", this.updateScale, { passive: true });
     this.startClock();
     this.initCharts();
     this.renderCharts();
@@ -476,16 +465,61 @@ export default {
     window.addEventListener("resize", this.onResize, { passive: true });
   },
   beforeDestroy() {
+    window.removeEventListener("resize", this.updateScale);
     window.removeEventListener("resize", this.onResize);
     clearInterval(this.timerClock);
     clearInterval(this.timerCycle);
     clearInterval(this.timerDutyScroll);
     this.onlineChart && this.onlineChart.dispose();
+    this.controlChart && this.controlChart.dispose();
     this.deviceChart && this.deviceChart.dispose();
   },
   methods: {
+    onMouseMove(e) {
+      const rect = this.$el.getBoundingClientRect();
+      this.mouseX = e.clientX - rect.left;
+      this.mouseY = e.clientY - rect.top;
+    },
+    onMouseLeave() {
+      this.mouseX = null;
+      this.mouseY = null;
+    },
+    updateScale() {
+      const w = window.innerWidth || this.baseW;
+      const h = window.innerHeight || this.baseH;
+      this.scale = Math.min(w / this.baseW, h / this.baseH);
+      this.$el && this.$el.style.setProperty("--s", this.scale.toFixed(6));
+    },
+    onDockMove(e) {
+      const rect = this.$el.getBoundingClientRect();
+      this.mouseX = e.clientX - rect.left;
+      this.mouseY = e.clientY - rect.top;
+    },
+    onDockLeave() {
+      this.mouseX = null;
+      this.mouseY = null;
+    },
+    navItemStyle(idx) {
+      if (this.mouseX == null) return { transform: "translateZ(0) scale(1)" };
+      const dock = this.$el.querySelector(".dockbar");
+      const el = dock && dock.children && dock.children[idx];
+      if (!el) return { transform: "translateZ(0) scale(1)" };
+      const r = el.getBoundingClientRect();
+      const root = this.$el.getBoundingClientRect();
+      const cx = (r.left - root.left) + r.width / 2;
+      const cy = (r.top - root.top) + r.height / 2;
+      const dx = this.mouseX - cx;
+      const dy = this.mouseY - cy;
+      const dist = Math.sqrt(dx * dx + dy * dy);
+      const R = 220 * this.scale;
+      const t = Math.max(0, 1 - dist / R);
+      const s = 1 + 0.35 * (t * t);
+      return { transform: `translateZ(0) scale(${s.toFixed(4)})` };
+    },
+
     onResize() {
       this.onlineChart && this.onlineChart.resize();
+      this.controlChart && this.controlChart.resize();
       this.deviceChart && this.deviceChart.resize();
     },
     startClock() {
@@ -503,11 +537,13 @@ export default {
 
     initCharts() {
       this.onlineChart = echarts.init(this.$refs.onlineChart);
+      this.controlChart = echarts.init(this.$refs.controlChart);
       this.deviceChart = echarts.init(this.$refs.deviceChart);
     },
 
     renderCharts() {
       this.renderOnlineChart();
+      this.renderControlChart();
       this.renderDeviceChart();
     },
 
@@ -556,6 +592,49 @@ export default {
       });
     },
 
+    renderControlChart() {
+      const items = this.controlLegend || [];
+      const total = this.controlTotal || items.reduce((s, x) => s + (x.v || 0), 0) || 0;
+      this.controlChart.setOption({
+        animation: true,
+        series: [
+          {
+            type: "pie",
+            radius: ["72%", "90%"],
+            center: ["50%", "50%"],
+            silent: true,
+            label: { show: false },
+            labelLine: { show: false },
+            data: items.map((x) => ({ value: x.v, name: x.t }))
+          }
+        ],
+        graphic: [
+          {
+            type: "text",
+            left: "center",
+            top: "40%",
+            style: {
+              text: `${total}个`,
+              fill: "rgba(235,255,255,0.92)",
+              fontSize: 18,
+              fontWeight: 800
+            }
+          },
+          {
+            type: "text",
+            left: "center",
+            top: "58%",
+            style: {
+              text: "控制信息",
+              fill: "rgba(190,225,255,0.70)",
+              fontSize: 12,
+              fontWeight: 600
+            }
+          }
+        ]
+      });
+    },
+
     renderDeviceChart() {
       const v = this.deviceView;
       this.deviceChart.setOption({
@@ -628,20 +707,37 @@ export default {
       };
       setTimeout(() => (this.mapPopup = null), 2200);
     },
-
-    scrollModules(dir) {
-      const max = Math.max(0, this.modules.length - 6);
-      this.navIndex = Math.max(0, Math.min(max, this.navIndex + dir));
+    assetUrl(file) {
+      try {
+        return require("@/assets/main/" + file);
+      } catch (e) {
+        return "";
+      }
+    },
+    navIconStyle(m) {
+      return { backgroundImage: `url(${this.assetUrl(m.img)})` };
     },
     selectModule(m) {
       this.activeModule = m.key;
-      // if (m.route) this.$router.push(m.route);
-    }
+      if (m.route && this.$router) this.$router.push(m.route);
+    },
+    dockPrev() {
+      if (typeof this.activeDockIndex !== "number") this.activeDockIndex = 0;
+      const n = (this.dockItems && this.dockItems.length) ? this.dockItems.length : 6;
+      this.activeDockIndex = (this.activeDockIndex - 1 + n) % n;
+    },
+    dockNext() {
+      if (typeof this.activeDockIndex !== "number") this.activeDockIndex = 0;
+      const n = (this.dockItems && this.dockItems.length) ? this.dockItems.length : 6;
+      this.activeDockIndex = (this.activeDockIndex + 1) % n;
+    },
   }
 };
 </script>
 
 <style scoped>
+
+
 /* ====== Root ====== */
 .page{
   width:100vw;
@@ -649,69 +745,43 @@ export default {
   overflow:hidden;
   background: radial-gradient(1200px 700px at 50% 25%, rgba(40,120,255,0.22), rgba(5,12,30,1) 58%);
   position:relative;
+  --s: 1;
   font-family: "Microsoft YaHei", system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
+  display:flex;
+  flex-direction:column;
 }
 
+
 /* ====== Top ====== */
 .topbar{
-  height: clamp(72px, 9vh, 98px);
+  height: calc(var(--s) * 100px);
   display:grid;
   grid-template-columns: 1fr 1.6fr 1fr;
   align-items:center;
-  padding: 0 clamp(12px, 2vw, 26px);
+  padding: 0 calc(var(--s) * 26px);
   position:relative;
+  background: center/100% 100% no-repeat;
+  background-image: url("~@/assets/main/main-header.png");
+  z-index: 10;
 }
-.topbar::before{
-  content:"";
-  position:absolute;
-  left:0; right:0; top:0; bottom:0;
-  background:
-    linear-gradient(180deg, rgba(10,30,80,0.65), rgba(10,30,80,0.15)),
-    linear-gradient(90deg, rgba(0,220,255,0.12), rgba(0,0,0,0) 18%, rgba(0,0,0,0) 82%, rgba(0,220,255,0.12));
-  border-bottom: 1px solid rgba(80,200,255,0.18);
-  pointer-events:none;
-}
-.top-left, .top-right{ position:relative; z-index:1; }
-.weather{ display:flex; align-items:center; gap:10px; color: rgba(230,250,255,0.92); }
-.dot-sun{
-  width:10px; height:10px; border-radius:999px;
-  background: radial-gradient(circle at 30% 30%, rgba(255,255,180,1), rgba(255,200,60,0.8));
-  box-shadow: 0 0 10px rgba(255,220,120,0.55);
-  display:inline-block;
-}
-.w1{ font-weight:800; letter-spacing:0.5px; }
-.w2{ color: rgba(200,235,255,0.85); }
-
-.top-center{ position:relative; z-index:1; display:flex; justify-content:center; }
+.topbar::before{ display:none; }
 .title-frame{
-  position:relative;
-  width: min(680px, 56vw);
-  height: clamp(46px, 5.6vh, 64px);
+  width: min(62vw, calc(var(--s) * 980px));
+  height: calc(var(--s) * 64px);
+  border-radius: calc(var(--s) * 14px);
+  background: rgba(10,35,70,0.35);
+  box-shadow: inset 0 0 calc(var(--s) * 26px) rgba(0,160,255,0.18);
   display:flex;
   align-items:center;
   justify-content:center;
-  border-radius: 12px;
-  background: linear-gradient(180deg, rgba(6,18,55,0.55), rgba(6,18,55,0.22));
-  border: 1px solid rgba(80,200,255,0.22);
-  box-shadow: 0 10px 34px rgba(0,0,0,0.28);
-  overflow:hidden;
-}
-.title-frame::before{
-  content:"";
-  position:absolute;
-  inset:-1px;
-  background: conic-gradient(from 180deg, rgba(0,240,255,0.0), rgba(0,240,255,0.22), rgba(0,240,255,0.0));
-  opacity:0.65;
-  filter: blur(12px);
 }
 .title{
-  position:relative;
-  font-size: clamp(22px, 2.2vw, 36px);
-  font-weight: 900;
-  letter-spacing: 2px;
-  color: rgba(235,255,255,0.96);
-  text-shadow: 0 2px 14px rgba(0,220,255,0.25);
+  font-size: calc(var(--s) * 34px);
+  letter-spacing: calc(var(--s) * 2px);
+  text-shadow: 0 0 calc(var(--s) * 10px) rgba(90, 200, 255, 0.35);
+  font-weight: 600;
 }
+
 .clock{ text-align:right; color: rgba(235,255,255,0.92); }
 .time{ font-weight: 900; font-size: clamp(18px, 1.6vw, 28px); letter-spacing: 1px; }
 .date{ margin-top:4px; font-size: 12px; color: rgba(190,225,255,0.78); }
@@ -719,17 +789,23 @@ export default {
 
 /* ====== Grid ====== */
 .grid{
-  height: calc(100vh - clamp(72px, 9vh, 98px) - clamp(88px, 11vh, 122px));
+  flex: 1;
+  min-height: 0;
   padding: clamp(10px, 1.6vw, 18px);
   display:grid;
   grid-template-columns: 1fr 2.1fr 1fr;
   gap: clamp(10px, 1.2vw, 16px);
 }
-.col{ min-width:0; display:flex; flex-direction:column; gap: clamp(10px, 1.2vw, 16px); }
+.col{ min-width:0; min-height:0; }
+.col.left, .col.right{ display:grid; grid-template-rows: 1fr 1fr 1fr; gap: clamp(10px, 1.2vw, 16px); }
+.col.mid{ display:flex; flex-direction:column; gap: clamp(10px, 1.2vw, 16px); }
 .grow{ flex:1; min-height:0; overflow:hidden; }
 
 /* ====== Cards ====== */
 .card{
+  display:flex;
+  flex-direction:column;
+  min-height:0;
   border-radius: 14px;
   background: linear-gradient(180deg, rgba(8,22,60,0.58), rgba(8,22,60,0.28));
   border: 1px solid rgba(80,200,255,0.18);
@@ -782,6 +858,8 @@ export default {
   font-weight:700;
   font-size: 12px;
 }
+.tabs.small{ margin-bottom: 8px; }
+.tabs.small .tab{ height: 26px; font-size: 12px; }
 .tab.active{
   background: rgba(0,180,255,0.18);
   color: rgba(235,255,255,0.92);
@@ -791,6 +869,10 @@ export default {
 /* Charts */
 .row{ position:relative; z-index:1; display:grid; grid-template-columns: 140px 1fr; gap: 12px; align-items:center; }
 .chart.donut{ width: 140px; height: 120px; }
+.chart.donut.small{ width: 120px; height: 108px; }
+.row.row-b{ grid-template-columns: 1fr 120px; }
+.legend.legend-b{ gap: 8px; }
+.legend.legend-b .lg{ grid-template-columns: 14px 1fr auto; }
 .legend{ display:flex; flex-direction:column; gap: 8px; }
 .lg{ display:flex; align-items:center; gap: 8px; color: rgba(200,235,255,0.78); font-size: 12px; }
 .lg .num{ margin-left:auto; color: rgba(235,255,255,0.92); font-weight: 800; }
@@ -826,7 +908,11 @@ export default {
 .pill.mute{ border-color: rgba(160,180,220,0.16); background: rgba(10,30,80,0.12); color: rgba(170,200,240,0.75); }
 
 /* Alarms */
-.alarm-list{ position:relative; z-index:1; height: 100%; overflow:auto; padding-right: 6px; }
+.alarm-list{
+  flex:1;
+  min-height:0;
+  overflow:auto;
+ position:relative; z-index:1; height: 100%; overflow:hidden; padding-right: 6px; }
 .alarm-item{
   display:flex;
   justify-content:space-between;
@@ -876,6 +962,7 @@ export default {
 /* Map */
 .map-wrap{ flex:1; min-height:0; }
 .map-frame{
+  position: relative;
   height:100%;
   border-radius: 16px;
   border: 1px solid rgba(80,200,255,0.18);
@@ -924,6 +1011,7 @@ export default {
 .caret{ opacity:0.8; }
 
 .map-canvas{
+  padding-bottom: calc(var(--s) * 120px);
   position:absolute;
   inset:0;
   background-image:
@@ -969,7 +1057,7 @@ export default {
 .legend-box{
   position:absolute;
   right: 14px;
-  bottom: 16px;
+  bottom: calc(var(--s) * 150px);
   width: 140px;
   border-radius: 14px;
   border: 1px solid rgba(80,200,255,0.16);
@@ -994,7 +1082,11 @@ export default {
 .c11{ background: rgba(255,90,90,0.90); }
 
 /* Tables */
-.tbl-wrap{ height:100%; overflow:hidden; }
+.tbl-wrap{
+  flex:1;
+  min-height:0;
+  overflow:auto;
+ height:100%; overflow:hidden; }
 .tbl{
   width:100%;
   border-collapse: collapse;
@@ -1048,14 +1140,18 @@ export default {
 
 /* ====== Bottom nav ====== */
 .bottombar{
-  position:absolute;
-  left:0; right:0; bottom:0;
+  flex: 0 0 auto;
   height: clamp(88px, 11vh, 122px);
   display:flex;
   align-items:center;
   justify-content:center;
-  gap: 10px;
-  padding: 0 14px;
+  padding: 0 14px 10px;
+}
+.nav-row{
+  display:flex;
+  align-items:center;
+  justify-content:center;
+  gap: clamp(10px, 1vw, 16px);
 }
 .bottombar::before{
   content:"";
@@ -1078,8 +1174,8 @@ export default {
   will-change: transform;
 }
 .nav-item{
-  width: 112px;
-  height: 88px;
+  width: clamp(92px, 6.8vw, 124px);
+  height: clamp(72px, 9vh, 96px);
   border-radius: 16px;
   border: 1px solid rgba(80,200,255,0.16);
   background: rgba(0,0,0,0.12);
@@ -1091,14 +1187,21 @@ export default {
   gap: 8px;
   color: rgba(200,235,255,0.76);
   position:relative;
+  transition: transform .16s ease, border-color .16s ease, box-shadow .16s ease;
 }
 .nav-item.active{
   background: rgba(0,200,255,0.10);
   color: rgba(235,255,255,0.92);
   box-shadow: 0 0 20px rgba(0,200,255,0.14);
 }
+.nav-item:hover{
+  transform: translateZ(0) scale(1.03);
+  border-color: rgba(0,220,255,0.26);
+  box-shadow: 0 0 20px rgba(0,220,255,0.10);
+}
 .nav-ic{
-  width: 44px; height: 44px;
+  width: clamp(58px, 4.8vw, 86px);
+  height: clamp(52px, 5.8vh, 78px);
   border-radius: 14px;
   display:flex;
   align-items:center;
@@ -1106,6 +1209,11 @@ export default {
   border: 1px solid rgba(80,200,255,0.18);
   background: rgba(10,30,80,0.22);
 }
+.nav-img{
+  width: 100%;
+  background: center/contain no-repeat;
+  filter: drop-shadow(0 0 10px rgba(80,200,255,0.35));
+}
 .nav-ic.glow{
   box-shadow: 0 0 18px rgba(0,220,255,0.22);
   background: rgba(0,200,255,0.10);
@@ -1149,4 +1257,142 @@ export default {
   .nav-viewport{ width: 94vw; }
   .map-frame{ min-height: 42vh; }
 }
+
+
+/* ====== Chart layout align with UI sample ====== */
+.card-a .row{ flex-direction: row-reverse; gap: calc(var(--s) * 14px); }
+.card-a .donut{ width: calc(var(--s) * 160px); height: calc(var(--s) * 160px); }
+.card-a .legend{ flex: 1; padding-right: calc(var(--s) * 6px); }
+
+.card-b .row-b{ flex-direction: row; gap: calc(var(--s) * 14px); }
+.card-b .donut.small{ width: calc(var(--s) * 150px); height: calc(var(--s) * 150px); }
+
+.card-r1 .row{ flex-direction: row-reverse; gap: calc(var(--s) * 14px); }
+.card-r1 .donut{ width: calc(var(--s) * 160px); height: calc(var(--s) * 160px); }
+
+
+/* ====== Bottom Dock (overlay on map) ====== */
+.dock-wrap{
+  position:absolute;
+  left:50%;
+  bottom: calc(var(--s) * 14px);
+  transform: translateX(-50%);
+  width: min(86%, calc(var(--s) * 980px)); /* 基本与地图主体宽度一致 */
+  display:flex;
+  justify-content:center;
+  pointer-events:auto;
+  z-index: 9;
+}
+.dockbar{
+  width: 100%;
+  display:flex;
+  align-items:flex-end;
+  justify-content:space-between;
+  gap: calc(var(--s) * 22px);
+  padding: calc(var(--s) * 6px) calc(var(--s) * 10px);
+}
+.dock-item{
+  width: calc(var(--s) * 98px);
+  height: calc(var(--s) * 124px);
+  display:flex;
+  flex-direction:column;
+  align-items:center;
+  justify-content:flex-start;
+  cursor:pointer;
+  transform-origin: 50% 80%;
+  transition: transform .08s linear;
+}
+.dock-icon{
+  width: calc(var(--s) * 98px);
+  height: calc(var(--s) * 86px);
+  background: center/contain no-repeat;
+  filter: drop-shadow(0 0 calc(var(--s) * 12px) rgba(70, 190, 255, .35));
+}
+.dock-label{
+  margin-top: calc(var(--s) * 10px);
+  font-size: calc(var(--s) * 14px);
+  line-height: 1;
+  opacity: .92;
+  text-shadow: 0 0 calc(var(--s) * 8px) rgba(0, 160, 255, .25);
+
+  color: rgba(235,255,255,0.92);
+  text-shadow: 0 0 calc(var(--s) * 10px) rgba(0,160,255,.35);
+}
+.dock-item:hover .dock-icon{
+  filter: drop-shadow(0 0 calc(var(--s) * 18px) rgba(120, 230, 255, .65));
+}
+
+
+
+
+/* ====== Patch: header corner info (match 示例图) ====== */
+.top-left, .top-right{
+  align-self: start;
+  padding-top: calc(var(--s) * 60px);
+}
+.weather{
+  display:flex;
+  align-items:center;
+  gap: calc(var(--s) * 10px);
+  color: rgba(235,255,255,0.92);
+}
+.dot-sun{
+  width: calc(var(--s) * 8px);
+  height: calc(var(--s) * 8px);
+  border-radius: 50%;
+  background: rgba(255,220,120,0.95);
+  box-shadow: 0 0 calc(var(--s) * 10px) rgba(255,220,120,0.55);
+  display:inline-block;
+}
+.w1{ font-size: calc(var(--s) * 18px); font-weight: 700; letter-spacing: 1px; }
+.w2{ font-size: calc(var(--s) * 16px); opacity: .9; }
+
+.clock{ text-align:right; color: rgba(235,255,255,0.92); }
+.time{
+  font-weight: 900;
+  font-size: calc(var(--s) * 28px);
+  line-height: 1;
+  letter-spacing: 1px;
+  text-shadow: 0 0 calc(var(--s) * 10px) rgba(90, 200, 255, 0.28);
+}
+.date{
+  margin-top: calc(var(--s) * 6px);
+  font-size: calc(var(--s) * 12px);
+  color: rgba(190,225,255,0.78);
+}
+
+/* ====== Patch: side cards sizing (avoid squashed/hidden) ====== */
+.left .card-a, .right .card-a{ flex: 0 0 calc(var(--s) * 220px); }
+.left .card-b{ flex: 0 0 calc(var(--s) * 240px); }
+.right .card-b{ flex: 0 0 calc(var(--s) * 230px); }
+.left .grow, .right .grow{ flex: 1 1 auto; min-height: calc(var(--s) * 230px); }
+
+/* ====== Patch: dock arrows + label color (match 示例图) ====== */
+.dock-wrap{
+  display:flex;
+  align-items:center;
+  justify-content:center;
+  gap: calc(var(--s) * 22px);
+}
+.dock-arrow{
+  width: calc(var(--s) * 54px);
+  height: calc(var(--s) * 54px);
+  background: center/contain no-repeat;
+  opacity: .92;
+  cursor: pointer;
+  transition: transform .18s ease, opacity .18s ease, filter .18s ease;
+  filter: drop-shadow(0 0 calc(var(--s) * 10px) rgba(60, 180, 255, .35));
+}
+.dock-arrow:hover{
+  opacity: 1;
+  transform: translateZ(0) scale(1.08);
+  filter: drop-shadow(0 0 calc(var(--s) * 14px) rgba(90, 210, 255, .6));
+}
+.dock-arrow.left{ background-image: url("~@/assets/main/main-left.png"); }
+.dock-arrow.right{ background-image: url("~@/assets/main/main-right.png"); }
+
+.dock-label{
+  color: rgba(235,255,255,0.92) !important;
+  text-shadow: 0 0 calc(var(--s) * 8px) rgba(0, 160, 255, 0.28);
+}
 </style>

+ 85 - 67
src/views/Login.vue

@@ -72,6 +72,9 @@ export default {
   components: { CaptchaCanvas },
   data() {
     return {
+      baseW: 1920,
+      baseH: 1080,
+      scale: 1,
       username: "admin",
       password: "123456",
       captchaCode: "",
@@ -81,7 +84,22 @@ export default {
       doorNavigated: false,
     };
   },
+  mounted() {
+    this.updateScale();
+    window.addEventListener('resize', this.updateScale, { passive: true });
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.updateScale);
+  },
+
   methods: {
+    updateScale() {
+      const w = window.innerWidth || this.baseW;
+      const h = window.innerHeight || this.baseH;
+      const s = Math.min(w / this.baseW, h / this.baseH);
+      this.scale = s;
+      this.$el && this.$el.style.setProperty('--s', s.toFixed(6));
+    },
     async onLogin() {
       this.hint = "";
 
@@ -102,14 +120,14 @@ export default {
         setTimeout(() => {
           if (this.doorNavigated) return;
           this.doorNavigated = true;
-          this.$router.push("/transition");
+          this.$router.push("/main");
         }, 1000);
       });
     },
     handleDoorEnd() {
       if (!this.isDoorOpening || this.doorNavigated) return;
       this.doorNavigated = true;
-      this.$router.push("/transition");
+      this.$router.push("/main");
     },
   }
 };
@@ -120,7 +138,8 @@ export default {
   width: 100vw; height: 100vh;
   position: relative;
   overflow: hidden;
-}
+  --s: 1;
+ }
 
 .bg{
   position:absolute;
@@ -133,7 +152,7 @@ export default {
 .header-deco{
   position:absolute;
   top: 0; left: 0; right: 0;
-  height: 86px;
+  height: calc(var(--s) * 86px);
   background: url("~@/assets/header_deco1.png") center/cover no-repeat;
   pointer-events:none;
   opacity: 0.95;
@@ -142,14 +161,14 @@ export default {
 
 .top-title{
   position:absolute;
-  top: 20px;
+  top: calc(var(--s) * 20px);
   left: 50%;
   transform: translateX(-50%);
-  font-size: 24px;
-  letter-spacing: 3px;
+  font-size: calc(var(--s) * 24px);
+  letter-spacing: calc(var(--s) * 3px);
   font-weight: 700;
   color: #e6f6ff;
-  text-shadow: 0 0 12px rgba(80,200,255,0.45);
+  text-shadow: 0 0 calc(var(--s) * 12px) rgba(80,200,255,0.45);
   pointer-events:none;
   z-index: 3; /* 关键:必须压过 content */
 }
@@ -159,9 +178,9 @@ export default {
   inset: 0;
   display:grid;
   grid-template-columns: 1fr 1fr;
-  gap: clamp(14px, 1.8vw, 28px);
-  padding: clamp(14px, 2.4vw, 30px);
-  padding-top: 60px; /* 给头部留空间 */
+  gap: clamp(calc(var(--s) * 14px), 1.8vw, calc(var(--s) * 28px));
+  padding: clamp(calc(var(--s) * 14px), 2.4vw, calc(var(--s) * 30px));
+  padding-top: calc(var(--s) * 60px); /* 给头部留空间 */
   align-items:center;
   z-index: 1;
 }
@@ -171,7 +190,7 @@ export default {
 .ellipse-area{
   position: relative;
   width: 65vw;          
-  max-width: 1150px;
+  max-width: calc(var(--s) * 1150px);
   height: 70vh;
 }
 
@@ -205,7 +224,7 @@ export default {
   );
   background-size: 200% 100%;
   animation: ellipse-flow 2.2s linear infinite;
-  filter: drop-shadow(0 0 10px rgba(43,220,255,0.18));
+  filter: drop-shadow(0 0 calc(var(--s) * 10px) rgba(43,220,255,0.18));
 }
 
 @keyframes ellipse-flow{
@@ -215,14 +234,14 @@ export default {
 
 .icon{
   position:absolute;
-  width: clamp(54px, 4.5vw, 78px);
-  height: clamp(54px, 4.5vw, 78px);
+  width: clamp(calc(var(--s) * 54px), 4.5vw, calc(var(--s) * 78px));
+  height: clamp(calc(var(--s) * 54px), 4.5vw, calc(var(--s) * 78px));
   border-radius: 50%;
   /* background: radial-gradient(circle at 30% 30%, rgba(120,220,255,0.4), rgba(10,35,80,0.4)); */
-  border: 1px solid rgba(60,180,255,0.35);
+  border: calc(var(--s) * 1px) solid rgba(60,180,255,0.35);
   box-shadow:
-      0 0 18px rgba(43,220,255,0.25),
-      inset 0 0 12px rgba(120,220,255,0.25);
+      0 0 calc(var(--s) * 18px) rgba(43,220,255,0.25),
+      inset 0 0 calc(var(--s) * 12px) rgba(120,220,255,0.25);
   transform: translate(-50%, -50%);
   animation: icon-float 3s ease-in-out infinite;
 }
@@ -236,14 +255,14 @@ export default {
 
 
 /* 单独给每个球不同浮动幅度(可选,效果更高级) */
-.icon-1{ --dy: 7px; }
-.icon-2{ --dy: 9px; }
-.icon-3{ --dy: 8px; }
-.icon-4{ --dy: 6px; }
+.icon-1{ --dy: calc(var(--s) * 7px); }
+.icon-2{ --dy: calc(var(--s) * 9px); }
+.icon-3{ --dy: calc(var(--s) * 8px); }
+.icon-4{ --dy: calc(var(--s) * 6px); }
 
 @keyframes icon-float{
   0%,100%{ transform: translate(-50%,-50%) translateY(0); }
-  50%{ transform: translate(-50%,-50%) translateY(calc(var(--dy, 8px) * -1)); }
+  50%{ transform: translate(-50%,-50%) translateY(calc(var(--dy, calc(var(--s) * 8px)) * -1)); }
 }
 .icon-1{ animation-duration: 3.2s; animation-delay: -0.2s; }
 .icon-2{ animation-duration: 3.8s; animation-delay: -1.1s; }
@@ -262,8 +281,8 @@ export default {
 .icon-4{ animation-duration: 4.2s, 3.5s; animation-delay: -2.7s, -3.0s; }
 
 @keyframes icon-glow{
-  0%,100%{ filter: drop-shadow(0 0 8px rgba(43,220,255,0.16)); }
-  50%{ filter: drop-shadow(0 0 16px rgba(43,220,255,0.36)); }
+  0%,100%{ filter: drop-shadow(0 0 calc(var(--s) * 8px) rgba(43,220,255,0.16)); }
+  50%{ filter: drop-shadow(0 0 calc(var(--s) * 16px) rgba(43,220,255,0.36)); }
 }
 /* 右侧面板 */
 .right{
@@ -272,7 +291,7 @@ export default {
   justify-content:center;
 }
 .panel{
-  width: min(680px, 30vw);
+  width: min(calc(var(--s) * 680px), 30vw);
   aspect-ratio: 900 / 680;
   position: relative;
   background: url("~@/assets/panel_frame.png") center/100% 100% no-repeat;
@@ -292,20 +311,20 @@ export default {
   width: 85%;
   font-size: var(--fs-title);
   color: rgba(220,250,255,0.92);
-  margin-bottom: 30px;
+  margin-bottom: calc(var(--s) * 30px);
   font-weight: 600;
 }
 .field{
   width: 85%;
-  height: 54px;
+  height: calc(var(--s) * 54px);
   display:flex;
   align-items:center;
-  gap: 10px;
-  padding: 0 16px;
+  gap: calc(var(--s) * 10px);
+  padding: 0 calc(var(--s) * 16px);
   background: linear-gradient(270deg, rgba(26,117,255,0.11) 0%, rgba(71,120,255,0.07) 100%);
-  border-radius: 8px;
-  border: 1px solid #3D72B8;
-  margin-bottom: 18px;
+  border-radius: calc(var(--s) * 8px);
+  border: calc(var(--s) * 1px) solid #3D72B8;
+  margin-bottom: calc(var(--s) * 18px);
 }
 
 .field{
@@ -317,31 +336,31 @@ export default {
   border-color: rgba(90, 220, 255, 0.95);
   background: linear-gradient(270deg, rgba(26,117,255,0.16) 0%, rgba(71,120,255,0.10) 100%);
   box-shadow:
-    0 0 0 1px rgba(110, 230, 255, 0.55),
-    0 0 18px rgba(43,220,255,0.28),
-    inset 0 0 14px rgba(120,220,255,0.18);
+    0 0 0 calc(var(--s) * 1px) rgba(110, 230, 255, 0.55),
+    0 0 calc(var(--s) * 18px) rgba(43,220,255,0.28),
+    inset 0 0 calc(var(--s) * 14px) rgba(120,220,255,0.18);
 }
 
 /* 可选:让左侧小图标也略微被点亮 */
 .field:focus-within .i{
   opacity: 1;
-  filter: drop-shadow(0 0 8px rgba(43,220,255,0.35));
+  filter: drop-shadow(0 0 calc(var(--s) * 8px) rgba(43,220,255,0.35));
 }
 
 
 .field-label{
-  flex: 0 0 44px;          /* “账号/密码/验证码”宽度固定 */
+  flex: 0 0 calc(var(--s) * 44px);          /* “账号/密码/验证码”宽度固定 */
   color: rgba(220,245,255,0.75);
-  font-size: 13px;
-  letter-spacing: 1px;
-  margin-right: 5px;
+  font-size: calc(var(--s) * 13px);
+  letter-spacing: calc(var(--s) * 1px);
+  margin-right: calc(var(--s) * 5px);
   user-select: none;
 }
 /* 关键:等比、居中、不拉伸 */
 .field .i{
-  width: 20px;
-  height: 20px;
-  flex: 0 0 20px;
+  width: calc(var(--s) * 20px);
+  height: calc(var(--s) * 20px);
+  flex: 0 0 calc(var(--s) * 20px);
   object-fit: contain;
   opacity: 0.92;
 }
@@ -353,7 +372,7 @@ export default {
   outline: none;
   background: transparent;
   color: rgba(235,255,255,0.92);
-  font-size: 14px;
+  font-size: calc(var(--s) * 14px);
 }
 
 .inp::placeholder{
@@ -363,8 +382,8 @@ export default {
 .row{
   width:85%;
   display:flex;
-  gap: 14px;
-  margin-bottom: 10px;
+  gap: calc(var(--s) * 14px);
+  margin-bottom: calc(var(--s) * 10px);
 }
 
 .cap-field{
@@ -377,30 +396,30 @@ export default {
 
 .btn{
   width: 85%;
-  height: 54px;
+  height: calc(var(--s) * 54px);
   border: none;
-  border-radius: 8px;
+  border-radius: calc(var(--s) * 8px);
   color: rgba(235,255,255,0.95);
   font-size: var(--fs-base);
   font-weight: 600;
   cursor: pointer;
   background: linear-gradient( 180deg, rgba(119,161,255,0) 0%, #77A1FF 100%);
-  border-radius: 8px;
-  border: 1px solid rgba(161,190,255,0.7);
+  border-radius: calc(var(--s) * 8px);
+  border: calc(var(--s) * 1px) solid rgba(161,190,255,0.7);
 }
 .btn:hover{ filter: brightness(1.08); }
 
 .hint{
-  margin-top: 12px;
+  margin-top: calc(var(--s) * 12px);
   color: rgba(255,180,180,0.92);
   font-size: var(--fs-base);
 }
 
 /* 响应式重排:窄屏时上下布局 */
-@media (max-width: 980px){
+@media (max-width: calc(var(--s) * 980px)){
   .content{
     grid-template-columns: 1fr;
-    padding: 18px;
+    padding: calc(var(--s) * 18px);
     align-items: start;
   }
   .ellipse-area{ width: 100%; height: 46vh; }
@@ -421,9 +440,9 @@ export default {
   /* === 你只需要调这三个:云彩环的中心点与半径 === */
   --cx: 27%;   /* 云彩环中心 x(大概在左侧 1/4) */
   --cy: 58%;   /* 云彩环中心 y(大概在中下) */
-  --r1: 145px; /* 内圈挖空半径(越大,AI 字越干净) */
-  --r2: 188px; /* 云彩环最亮区域半径 */
-  --r3: 288px; /* 外圈渐隐半径(越大,环越厚/越散) */
+  --r1: calc(var(--s) * 145px); /* 内圈挖空半径(越大,AI 字越干净) */
+  --r2: calc(var(--s) * 188px); /* 云彩环最亮区域半径 */
+  --r3: calc(var(--s) * 288px); /* 外圈渐隐半径(越大,环越厚/越散) */
 
   /* ✅只保留“环形”区域:透明(内) -> 显示(环) -> 透明(外) */
   -webkit-mask-image: radial-gradient(circle at var(--cx) var(--cy),
@@ -439,7 +458,7 @@ export default {
 
   /* ✅增强可见度(科技蓝) */
   mix-blend-mode: lighten;
-  filter: drop-shadow(0 0 40px rgba(60,220,255,.55)) drop-shadow(0 0 110px rgba(60,220,255,.25));
+  filter: drop-shadow(0 0 calc(var(--s) * 40px) rgba(60,220,255,.55)) drop-shadow(0 0 calc(var(--s) * 110px) rgba(60,220,255,.25));
 
   /* ✅慢旋转 + 呼吸淡入淡出 */
   opacity: .35;
@@ -453,9 +472,9 @@ export default {
 }
 
 @keyframes cloud-breathe{
-  0%{ opacity: .18; filter: drop-shadow(0 0 22px rgba(60,220,255,.35)) drop-shadow(0 0 70px rgba(60,220,255,.18)); }
-  50%{ opacity: .55; filter: drop-shadow(0 0 55px rgba(60,220,255,.70)) drop-shadow(0 0 140px rgba(60,220,255,.35)); }
-  100%{ opacity: .22; filter: drop-shadow(0 0 28px rgba(60,220,255,.40)) drop-shadow(0 0 85px rgba(60,220,255,.20)); }
+  0%{ opacity: .18; filter: drop-shadow(0 0 calc(var(--s) * 22px) rgba(60,220,255,.35)) drop-shadow(0 0 calc(var(--s) * 70px) rgba(60,220,255,.18)); }
+  50%{ opacity: .55; filter: drop-shadow(0 0 calc(var(--s) * 55px) rgba(60,220,255,.70)) drop-shadow(0 0 calc(var(--s) * 140px) rgba(60,220,255,.35)); }
+  100%{ opacity: .22; filter: drop-shadow(0 0 calc(var(--s) * 28px) rgba(60,220,255,.40)) drop-shadow(0 0 calc(var(--s) * 85px) rgba(60,220,255,.20)); }
 }
 
 /* 开门层:覆盖在 bg 上方(z-index 0),不挡 content(content 是 1) */
@@ -465,7 +484,7 @@ export default {
   z-index: 0;
   pointer-events: none;
   opacity: 1; 
-  perspective: 1200px;
+  perspective: calc(var(--s) * 1200px);
 }
 .door{
   transform-origin: center;
@@ -508,7 +527,7 @@ export default {
   position:absolute;
   left:50%;
   top:0;
-  width:2px;
+  width:calc(var(--s) * 2px);
   height:100%;
   transform: translateX(-50%);
   opacity: 0;
@@ -518,8 +537,8 @@ export default {
     rgba(120,240,255,0.95),
     rgba(0,0,0,0)
   );
-  filter: drop-shadow(0 0 18px rgba(60,220,255,.85))
-          drop-shadow(0 0 55px rgba(60,220,255,.35));
+  filter: drop-shadow(0 0 calc(var(--s) * 18px) rgba(60,220,255,.85))
+          drop-shadow(0 0 calc(var(--s) * 55px) rgba(60,220,255,.35));
 }
 
 .split-door.opening::after{
@@ -567,5 +586,4 @@ export default {
   from{ opacity: 0; }
   to{ opacity: 0.95; } /* 想更暗就 0.95 */
 }
-
-</style>
+</style>

+ 325 - 0
src/views/Main.vue

@@ -0,0 +1,325 @@
+<template>
+  <div class="main-page" @mousemove="onMouseMove" @mouseleave="onMouseLeave">
+    <!-- 背景 -->
+    <div class="bg"></div>
+
+    <!-- 顶部 Header -->
+    <div class="header">
+      <!-- <div class="title">交通信号控制平台</div> -->
+    </div>
+
+    <!-- 漂浮闪烁点 -->
+    <div class="spark-layer" aria-hidden="true">
+      <span
+        v-for="d in dots"
+        :key="d.id"
+        class="spark"
+        :style="dotStyle(d)"
+      />
+    </div>
+
+    <!-- 中央功能区 -->
+    <div class="center-wrap">
+      <div class="nav-arrow left" @click="onPrev" title="上一页"></div>
+
+      <div class="dock">
+        <div
+          v-for="(item, idx) in items"
+          :key="item.key"
+          class="dock-item"
+          :style="itemStyle(idx)"
+          @click="go(item)"
+        >
+          <div class="icon" :style="iconStyle(item)"></div>
+          <div class="label">{{ item.label }}</div>
+        </div>
+      </div>
+
+      <div class="nav-arrow right" @click="onNext" title="下一页"></div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "Main",
+  data() {
+    return {
+      // 以 1920x1080 为设计基准,用 scale 把“设计像素”映射到任意大屏
+      baseW: 1920,
+      baseH: 1080,
+      scale: 1,
+
+      // mac dock 效果:鼠标点位
+      mouseX: null,
+      mouseY: null,
+
+            // 6 个功能(route 先对齐到你当前路由:目前只有 /home 可落地,其它模块先通过 query 标记,避免 404)
+      items: [
+        { key: "home", label: "首页", img: "main-home.png", route: { path: "/home" } },
+        { key: "watch", label: "状态监控", img: "main-watch.png", route: { path: "/home", query: { panel: "watch" } } },
+        { key: "security", label: "特勤安保", img: "main-security.png", route: { path: "/home", query: { panel: "security" } } },
+        { key: "coor", label: "干线协调", img: "main-coor.png", route: { path: "/home", query: { panel: "coor" } } },
+        { key: "surve", label: "状态监控", img: "main-surve.png", route: { path: "/home", query: { panel: "surve" } } },
+        { key: "setting", label: "系统设置", img: "main-setting.png", route: { path: "/home", query: { panel: "setting" } } },
+      ],
+
+      // 亮点
+      dots: [],
+    };
+  },
+  mounted() {
+    this.updateScale();
+    window.addEventListener("resize", this.updateScale, { passive: true });
+    this.initDots();
+  },
+  beforeDestroy() {
+    window.removeEventListener("resize", this.updateScale);
+  },
+  methods: {
+    updateScale() {
+      const w = window.innerWidth || this.baseW;
+      const h = window.innerHeight || this.baseH;
+      // 取 min 保证不裁切(大屏常见)
+      this.scale = Math.min(w / this.baseW, h / this.baseH);
+      // 写到 css 变量,方便 calc(var(--s) * xxxpx)
+      this.$el && this.$el.style.setProperty("--s", this.scale.toFixed(6));
+    },
+
+    // ---------- Dock 缩放 ----------
+    onMouseMove(e) {
+      const rect = this.$el.getBoundingClientRect();
+      this.mouseX = e.clientX - rect.left;
+      this.mouseY = e.clientY - rect.top;
+    },
+    onMouseLeave() {
+      this.mouseX = null;
+      this.mouseY = null;
+    },
+    itemStyle(idx) {
+      if (this.mouseX == null) return { transform: "translateZ(0) scale(1)" };
+
+      const dock = this.$el.querySelector(".dock");
+      const el = dock && dock.children && dock.children[idx];
+      if (!el) return { transform: "translateZ(0) scale(1)" };
+
+      const r = el.getBoundingClientRect();
+      const root = this.$el.getBoundingClientRect();
+      const cx = (r.left - root.left) + r.width / 2;
+      const cy = (r.top - root.top) + r.height / 2;
+
+      const dx = this.mouseX - cx;
+      const dy = this.mouseY - cy;
+      const dist = Math.sqrt(dx * dx + dy * dy);
+
+      // 影响半径:约 220 设计像素(随 scale 变化)
+      const R = 220 * this.scale;
+      const t = Math.max(0, 1 - dist / R); // 0~1
+      const s = 1 + 0.35 * (t * t); // 最大放大 1.35
+
+      return { transform: `translateZ(0) scale(${s.toFixed(4)})` };
+    },
+
+    // ---------- 资源映射 ----------
+    assetUrl(file) {
+      try {
+        return require("@/assets/main/" + file);
+      } catch (e) {
+        return "";
+      }
+    },
+    iconStyle(item) {
+      return {
+        backgroundImage: `url(${this.assetUrl(item.img)})`,
+      };
+    },
+
+    // ---------- 导航 ----------
+        go(item) {
+      // 路由对齐:你的路由表里目前只有 /home(其它页面后续再补路由)
+      // 先用 query.panel 区分模块,保证点击不会 404
+      if (this.$router && item.route) this.$router.push(item.route);
+    }
+    ,
+    onPrev() {
+      this.$emit("prev");
+    },
+    onNext() {
+      this.$emit("next");
+    },
+
+    // ---------- 闪烁点 ----------
+    initDots() {
+      const count = 28;
+      const arr = [];
+      for (let i = 0; i < count; i++) {
+        arr.push({
+          id: i,
+          x: 18 + Math.random() * 64, // 百分比布局,适配任意屏幕
+          y: 22 + Math.random() * 56,
+          r: 1.2 + Math.random() * 1.8,
+          a: 0.45 + Math.random() * 0.55,
+          d: Math.random() * 2.8, // delay
+          t: 1.8 + Math.random() * 2.6, // duration
+        });
+      }
+      this.dots = arr;
+    },
+    dotStyle(d) {
+      return {
+        left: d.x + "%",
+        top: d.y + "%",
+        width: `calc(var(--s) * ${d.r * 4}px)`,
+        height: `calc(var(--s) * ${d.r * 4}px)`,
+        opacity: d.a,
+        animationDelay: `${d.d}s`,
+        animationDuration: `${d.t}s`,
+      };
+    },
+  },
+};
+</script>
+
+<style scoped>
+.main-page{
+  position: relative;
+  width: 100vw;
+  height: 100vh;
+  overflow: hidden;
+  --s: 1;
+  color: #d9efff;
+  user-select: none;
+}
+
+/* 背景图(图2) */
+.bg{
+  position:absolute;
+  inset:0;
+  background: center/cover no-repeat;
+  background-image: url("~@/assets/main/main-bg.png");
+  filter: saturate(1.05) contrast(1.03);
+}
+
+/* 顶部 header(高度 100 设计像素) */
+.header{
+  position:absolute;
+  left:0;
+  top:0;
+  width:100%;
+  height: calc(var(--s) * 100px);
+  background: center/100% 100% no-repeat;
+  background-image: url("~@/assets/main/main-header.png");
+  display:flex;
+  align-items:center;
+  justify-content:center;
+  z-index: 5;
+}
+.title{
+  font-size: calc(var(--s) * 34px);
+  letter-spacing: calc(var(--s) * 2px);
+  text-shadow: 0 0 calc(var(--s) * 10px) rgba(90, 200, 255, 0.35);
+  font-weight: 600;
+}
+
+/* 中间区域:使用居中 + 轻微上移(避免写死 1920 的绝对 px) */
+.center-wrap{
+  position:absolute;
+  left:50%;
+  top:46%;
+  transform: translate(-50%, -50%);
+  width: min(92vw, calc(var(--s) * 1600px));
+  display:flex;
+  align-items:center;
+  justify-content:center;
+  gap: calc(var(--s) * 40px);
+  z-index: 6;
+}
+
+/* 左右箭头(54*54) */
+.nav-arrow{
+  width: calc(var(--s) * 54px);
+  height: calc(var(--s) * 54px);
+  background: center/contain no-repeat;
+  opacity: .92;
+  cursor: pointer;
+  transition: transform .18s ease, opacity .18s ease, filter .18s ease;
+  filter: drop-shadow(0 0 calc(var(--s) * 10px) rgba(60, 180, 255, .35));
+}
+.nav-arrow:hover{
+  opacity: 1;
+  transform: translateZ(0) scale(1.08);
+  filter: drop-shadow(0 0 calc(var(--s) * 14px) rgba(90, 210, 255, .6));
+}
+.nav-arrow.left{ background-image: url("~@/assets/main/main-left.png"); }
+.nav-arrow.right{ background-image: url("~@/assets/main/main-right.png"); }
+
+/* 功能按钮 Dock */
+.dock{
+  display:flex;
+  align-items:flex-start;
+  justify-content:center;
+  gap: calc(var(--s) * 45px);
+  padding: calc(var(--s) * 12px) calc(var(--s) * 16px);
+  border-radius: calc(var(--s) * 18px);
+  background: linear-gradient(180deg, rgba(10,35,70,.18), rgba(0,0,0,.08));
+  box-shadow: 0 0 calc(var(--s) * 26px) rgba(0, 140, 255, .08) inset;
+  backdrop-filter: blur(2px);
+  margin: 0 6%;
+}
+
+.dock-item{
+  width: calc(var(--s) * 98px);
+  height: calc(var(--s) * 124px);
+  display:flex;
+  flex-direction: column;
+  align-items:center;
+  justify-content:flex-start;
+  cursor:pointer;
+  transform-origin: 50% 80%;
+  transition: transform .08s linear;
+}
+
+.icon{
+  width: calc(var(--s) * 98px);
+  height: calc(var(--s) * 86px);
+  background: center/contain no-repeat;
+  filter: drop-shadow(0 0 calc(var(--s) * 12px) rgba(70, 190, 255, .35));
+}
+
+.label{
+  margin-top: calc(var(--s) * 10px);
+  font-size: calc(var(--s) * 16px);
+  line-height: 1;
+  opacity: .92;
+  text-shadow: 0 0 calc(var(--s) * 8px) rgba(0, 160, 255, .25);
+}
+
+/* hover 细节:轻微发光 + label 提亮 */
+.dock-item:hover .icon{
+  filter: drop-shadow(0 0 calc(var(--s) * 18px) rgba(120, 230, 255, .65));
+}
+.dock-item:hover .label{
+  opacity: 1;
+}
+
+/* 闪烁亮点 */
+.spark-layer{
+  position:absolute;
+  inset:0;
+  z-index: 4;
+  pointer-events: none;
+}
+.spark{
+  position:absolute;
+  border-radius: 999px;
+  background: radial-gradient(circle, rgba(140, 235, 255, .95), rgba(140, 235, 255, 0) 70%);
+  animation-name: twinkle;
+  animation-timing-function: ease-in-out;
+  animation-iteration-count: infinite;
+}
+@keyframes twinkle{
+  0%{ transform: translateZ(0) scale(.7); opacity: .22; }
+  50%{ transform: translateZ(0) scale(1.25); opacity: .9; }
+  100%{ transform: translateZ(0) scale(.7); opacity: .22; }
+}
+</style>