6 Коммиты 83a0045e2c ... 48db01680f

Автор SHA1 Сообщение Дата
  画安 48db01680f 登录视频优化脚本: H265转H264 + moov前置 + CRF31降码率800kbps + месяцев назад: 2
  画安 3be6e4a501 修复标题字体加载慢导致换行: @font-face 加 font-display:swap + 标题 white-space:nowrap месяцев назад: 2
  画安 3ddd675e7e 主导航页面添加logo месяцев назад: 2
  画安 aaebce62f8 添加404页面 месяцев назад: 2
  画安 50aacbb176 路口详情-当前阶段: 跟随扫描线自动高亮 + 单行布局 + 支持全部阶段 месяцев назад: 2
  画安 d2787803a3 多窗口(CrossingMultiView)启用拖拽: 三个页面 openCrossingMultiView 配置 draggable 改为 true месяцев назад: 2

+ 1 - 1
public/index.html

@@ -5,7 +5,7 @@
     <meta http-equiv="X-UA-Compatible" content="IE=edge">
     <meta name="viewport" content="width=device-width,initial-scale=1.0">
     <link rel="icon" href="<%= BASE_URL %>favicon.ico">
-    
+
     <link rel="preconnect" href="https://cdn.staticfile.org">
 
     <script src="<%= BASE_URL %>config.js"></script>

BIN
src/assets/login/login-bg.mp4


+ 19 - 10
src/components/ui/CrossingDetailPanel.vue

@@ -374,6 +374,15 @@ export default {
             // 更新实时方案圆饼图的已走时长
             this.updateRealtimeDonut(activeTime);
 
+            // 非手动模式下,根据扫描线位置自动切换当前阶段高亮
+            if (!this.isManualMode && this.phaseStages.length > 0) {
+                const idx = this.phaseStages.findIndex(s => activeTime >= s.start && activeTime < s.end);
+                if (idx >= 0) {
+                    const newStage = String(idx + 1);
+                    if (this.currentStage !== newStage) this.currentStage = newStage;
+                }
+            }
+
             // 双相位图模式:检测周期 wrap-around,触发 refetch 让后端把刚结束的周期作为 lastCycle 返回
             if (this.isDual) {
                 if (this._prevTick != null && activeTime < this._prevTick - 1) {
@@ -393,22 +402,22 @@ export default {
                 this._refetching = false;
             }
         },
-        // 从 phaseData 解析4个阶段的绿灯时长,构建圆饼图数据
+        // 从 phaseData 解析所有阶段的绿灯时长,构建圆饼图数据
         buildDonutFromPhaseData() {
             const phaseData = this.mockPhaseData || [];
-            const stageColors = ['#3b82f6', '#a855f7', '#14b8a6', '#f59e0b'];
+            const stageColors = ['#3b82f6', '#a855f7', '#14b8a6', '#f59e0b', '#ec4899', '#06b6d4', '#84cc16', '#f97316'];
             const stageLabels = ['1-南北直行', '2-南北左转', '3-东西直行', '4-东西左转'];
 
-            // 提取 track 0 的绿灯相位(每阶段的第一个 green)
+            // 提取 track 0 的绿灯相位
             const greenPhases = phaseData.filter(p => p[0] === 0 && p[5] === 'green');
-            const stages = greenPhases.slice(0, 4).map((p, i) => {
-                const total = p[8] || Math.floor(this.cycleLength / 4);
+            const stages = greenPhases.map((p, i) => {
+                const total = p[8] || Math.floor(this.cycleLength / greenPhases.length);
                 return {
                     label: stageLabels[i] || `阶段${i + 1}`,
                     value: total,           // 阶段总时长
                     start: p[1],            // 阶段开始时间(从绿灯起始)
                     end: p[1] + total,      // 阶段结束时间
-                    color: stageColors[i]
+                    color: stageColors[i % stageColors.length]
                 };
             });
             this.phaseStages = stages;
@@ -450,8 +459,8 @@ export default {
                 }))
             ];
         },
-        // 观察首个 stage-item-wrapper, 算出 2 行高度(含行间 gap) 写到 CSS 变量。
-        // grid 的 height 锁在 2 行高度: 不足 8 个时下排留白; 超 8 个时按行滚动。
+        // 观察首个 stage-item-wrapper, 算出 1 行高度写到 CSS 变量。
+        // grid 的 height 锁在 1 行高度: 超 4 个时按行滚动。
         // 防 ResizeObserver loop 警告: rAF 推到下一帧 + 值比较切断"测量→撑大→再测"循环。
         _observeFirstStageItem() {
             const grid = this.$refs.stageGrid;
@@ -472,7 +481,7 @@ export default {
                     if (rowH <= 0) return;
                     const cs = window.getComputedStyle(grid);
                     const gap = parseFloat(cs.rowGap || cs.gap || '0') || 0;
-                    const reservedH = rowH * 2 + gap;
+                    const reservedH = rowH;
                     if (this._lastRowH === rowH && this._lastReservedH === reservedH) return;
                     this._lastRowH = rowH;
                     this._lastReservedH = reservedH;
@@ -1143,7 +1152,7 @@ export default {
     opacity: 0;
 }
 
-/* 4 列固定网格; 始终锁 2 行 + 行间 gap (不足 8 阶段时第 2 行留白); 超 8 滚动 */
+/* 4 列固定网格; 锁 1 行高度; 超 4 阶段纵向滚动 */
 .current-stage {
     margin-bottom: clamp(4px, calc(var(--s) * 10px), 20px);
     display: grid;

+ 1 - 0
src/layouts/DashboardLayout.vue

@@ -228,6 +228,7 @@ export default {
     text-align: center;
     font-style: normal;
     text-transform: none;
+    white-space: nowrap;
 }
 
 .frame-left {

+ 16 - 0
src/layouts/LoginLayout.vue

@@ -1,6 +1,9 @@
 <template>
     <div class="fluid-dashboard">
         <div class="frame-top">
+            <div class="top-logo">
+                <img :src="brand.logo" />
+            </div>
             <div class="title glow-text" :data-text="brand.title">{{ brand.title }}</div>
             <div class="top-right-actions">
                 <FullscreenToggle />
@@ -84,6 +87,18 @@ export default {
     background-size: 100% 100%;
 }
 
+.top-logo {
+    position: absolute;
+    top: 10px;
+    left: 100px;
+    height: 30px;
+}
+.top-logo img {
+    width: 100%;
+    height: 100%;
+    object-fit: contain;
+}
+
 .top-right-actions {
     position: absolute;
     top: 2px;
@@ -104,6 +119,7 @@ export default {
     text-align: center;
     font-style: normal;
     text-transform: none;
+    white-space: nowrap;
 }
 
 .frame-left {

+ 2 - 0
src/router/index.js

@@ -8,6 +8,7 @@ import StatusMonitoring from "@/views/StatusMonitoring.vue";
 import SpecialSituationMonitoring from "@/views/SpecialSituationMonitoring.vue";
 import DataAnalysis from "@/views/DataAnalysis.vue";
 import TrunkCoordination from "@/views/TrunkCoordination.vue";
+import NotFound from "@/views/NotFound.vue";
 
 Vue.use(Router);
 
@@ -22,5 +23,6 @@ export default new Router({
     { path: "/surve", component: StatusMonitoring },
     { path: "/security", component: SpecialSituationMonitoring },
     { path: "/trunk", component: TrunkCoordination },
+    { path: "*", component: NotFound },
   ]
 });

+ 1 - 0
src/styles/base.css

@@ -7,6 +7,7 @@
   src: url('../assets/fonts/YouSheBiaoTiHei_Regular.ttf') format('truetype');
   font-weight: normal;
   font-style: normal;
+  font-display: swap;
 }
 
 

+ 4 - 5
src/views/Login.vue

@@ -83,11 +83,11 @@ export default {
 }
 .bg-video {
   position: absolute;
-  top: 50%;
+  top: 0;
   left: 50%;
-  transform: translate(-50%, -50%);
+  transform: translateX(-50%);
   width: 100%;
-  height: 100%;
+  height: 120%;
   object-fit: cover;
   z-index: 1;
 }
@@ -163,6 +163,7 @@ export default {
   text-align: center;
   font-style: normal;
   text-transform: none;
+  white-space: nowrap;
 }
 
 .right-panel {
@@ -180,9 +181,7 @@ export default {
   text-shadow: 0 0 8px rgba(0, 150, 255, 0.6), 0 1px 3px rgba(0, 0, 0, 0.8);
   letter-spacing: 1px;
 }
-</style>
 
-<style>
 :fullscreen .login-root .top-bar {
   height: 14vh;
 }

+ 130 - 0
src/views/NotFound.vue

@@ -0,0 +1,130 @@
+<template>
+  <div class="not-found">
+    <div class="content">
+      <div class="code">
+        <span class="digit">4</span>
+        <span class="digit zero">0</span>
+        <span class="digit">4</span>
+      </div>
+      <div class="divider"></div>
+      <p class="message">页面未找到</p>
+      <p class="hint">您访问的页面不存在或已被移除</p>
+      <button class="btn-back" @click="goBack">返回首页</button>
+    </div>
+    <div class="grid-bg"></div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'NotFound',
+  methods: {
+    goBack() {
+      this.$router.push('/login');
+    }
+  }
+}
+</script>
+
+<style scoped>
+.not-found {
+  position: fixed;
+  inset: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: radial-gradient(ellipse at 50% 40%, #0a1628 0%, #050d1a 100%);
+  overflow: hidden;
+  font-family: 'Helvetica Neue', Arial, sans-serif;
+}
+
+.grid-bg {
+  position: absolute;
+  inset: 0;
+  background-image:
+    linear-gradient(rgba(59, 130, 246, 0.04) 1px, transparent 1px),
+    linear-gradient(90deg, rgba(59, 130, 246, 0.04) 1px, transparent 1px);
+  background-size: 60px 60px;
+  pointer-events: none;
+}
+
+.content {
+  position: relative;
+  z-index: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 20px;
+}
+
+.code {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.digit {
+  font-size: 120px;
+  font-weight: 800;
+  line-height: 1;
+  color: transparent;
+  background: linear-gradient(180deg, #3b82f6 0%, #1e3a6e 100%);
+  -webkit-background-clip: text;
+  background-clip: text;
+  text-shadow: 0 0 40px rgba(59, 130, 246, 0.3);
+  user-select: none;
+}
+
+.digit.zero {
+  animation: pulse 2.5s ease-in-out infinite;
+}
+
+@keyframes pulse {
+  0%, 100% { opacity: 1; }
+  50% { opacity: 0.4; }
+}
+
+.divider {
+  width: 80px;
+  height: 2px;
+  background: linear-gradient(90deg, transparent, #3b82f6, transparent);
+  border-radius: 1px;
+}
+
+.message {
+  font-size: 22px;
+  font-weight: 600;
+  color: #e0e6f1;
+  margin: 0;
+  letter-spacing: 4px;
+}
+
+.hint {
+  font-size: 14px;
+  color: #758599;
+  margin: 0;
+}
+
+.btn-back {
+  margin-top: 12px;
+  padding: 10px 36px;
+  font-size: 14px;
+  color: #ffffff;
+  background: rgba(59, 130, 246, 0.15);
+  border: 1px solid rgba(59, 130, 246, 0.4);
+  border-radius: 4px;
+  cursor: pointer;
+  transition: all 0.25s;
+  letter-spacing: 2px;
+}
+
+.btn-back:hover {
+  background: rgba(59, 130, 246, 0.3);
+  border-color: rgba(59, 130, 246, 0.7);
+  box-shadow: 0 0 20px rgba(59, 130, 246, 0.2);
+}
+
+.btn-back:active {
+  transform: translateY(1px);
+}
+</style>

+ 1 - 1
src/views/SpecialSituationMonitoring.vue

@@ -441,7 +441,7 @@ export default {
                 showClose: false,
                 noPadding: true,
                 enableDblclickExpand: false,
-                draggable: false,
+                draggable: true,
                 bringToFrontOnMousedown: false,
                 data: {
                     crossings: [...this.crossingSelections],

+ 1 - 1
src/views/StatusMonitoring.vue

@@ -567,7 +567,7 @@ export default {
                 showClose: false,
                 noPadding: true,
                 enableDblclickExpand: false,
-                draggable: false,
+                draggable: true,
                 bringToFrontOnMousedown: false,
                 data: {
                     crossings: [...this.crossingSelections],

+ 1 - 1
src/views/TrunkCoordination.vue

@@ -445,7 +445,7 @@ export default {
                 showClose: false,
                 noPadding: true,
                 enableDblclickExpand: false,
-                draggable: false,
+                draggable: true,
                 bringToFrontOnMousedown: false,
                 data: {
                     crossings: [...this.crossingSelections],