Explorar o código

新增SeamlessScroll滚动组件;修改首页的勤务执行的表格滚动;修改TechTable的样式;关闭了状态监控页面的特勤的初始弹窗;

画安 hai 1 mes
pai
achega
fac5845916

+ 128 - 0
src/components/ui/SeamlessScroll.vue

@@ -0,0 +1,128 @@
+<template>
+  <div class="seamless-scroll-container" ref="scrollRef" @mouseenter="pause" @mouseleave="resume">
+    <slot :list="scrollData"></slot>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'SeamlessScroll',
+  props: {
+    data: { type: Array, required: true },
+    limit: { type: Number, default: 4 },
+    speed: { type: Number, default: 0.5 },
+    measureSelector: { type: String, default: 'tbody' }
+  },
+  data() {
+    return {
+      scrollTimer: null,
+      currentTop: 0,
+      resetHeight: 0,
+      isScrollable: false
+    }
+  },
+  computed: {
+    scrollData() {
+      if (this.data && this.data.length > this.limit) {
+        this.isScrollable = true;
+        const clone = JSON.parse(JSON.stringify(this.data)).map((item, index) => ({
+          ...item,
+          _clone_id: `clone_${Date.now()}_${index}`
+        }));
+        return [...this.data, ...clone];
+      }
+      this.isScrollable = false;
+      return this.data; 
+    }
+  },
+  // 加入 mounted 钩子,确保 DOM 绝对渲染完毕再测算
+  mounted() {
+    console.log('✅ SeamlessScroll: 组件已挂载,准备初始化滚动');
+    this.initScroll();
+  },
+  watch: {
+    data: {
+      handler() { 
+        console.log('🔄 SeamlessScroll: 监测到数据变化');
+        this.initScroll(); 
+      },
+      deep: true
+    }
+  },
+  beforeDestroy() {
+    this.pause();
+  },
+  methods: {
+    initScroll() {
+      this.pause();
+      this.currentTop = 0;
+      if (this.$refs.scrollRef) this.$refs.scrollRef.scrollTop = 0;
+
+      if (!this.isScrollable) {
+        console.log('🛑 SeamlessScroll: 数据量不足,无需滚动');
+        return;
+      }
+
+      this.$nextTick(() => {
+        // 加大一点延时,给表格充足的撑开时间
+        setTimeout(() => {
+          const wrapper = this.$refs.scrollRef;
+          if (!wrapper) return;
+          
+          const measureEl = wrapper.querySelector(this.measureSelector);
+
+          // 【排查神器】:打印高度对比
+        //   console.log(`📏 尺寸核对 -> 容器可视高度: ${wrapper.clientHeight}px, 内容真实总高: ${wrapper.scrollHeight}px`);
+
+          // 如果容器高度等于或大于内容高度,说明没有溢出,肯定滚不动
+          if (wrapper.scrollHeight <= wrapper.clientHeight) {
+            console.warn('⚠️ SeamlessScroll 警告: 内容高度没有超出容器高度,滚动被迫终止!请检查外部 CSS 高度限制。');
+            return;
+          }
+
+          this.resetHeight = measureEl ? measureEl.offsetHeight / 2 : wrapper.scrollHeight / 2;
+        //   console.log('🚀 SeamlessScroll: 滚动初始化成功!复位锚点高度为:', this.resetHeight);
+
+          this.resume();
+        }, 100); 
+      });
+    },
+    resume() {
+      if (!this.isScrollable || this.resetHeight <= 0) return;
+      const step = () => {
+        const wrapper = this.$refs.scrollRef;
+        if (!wrapper) return;
+
+        this.currentTop += this.speed;
+        wrapper.scrollTop = Math.floor(this.currentTop);
+
+        if (wrapper.scrollTop >= this.resetHeight) {
+          this.currentTop = 0;
+          wrapper.scrollTop = 0;
+        }
+        this.scrollTimer = requestAnimationFrame(step);
+      };
+      this.scrollTimer = requestAnimationFrame(step);
+    },
+    pause() {
+      if (this.scrollTimer) {
+        cancelAnimationFrame(this.scrollTimer);
+        this.scrollTimer = null;
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.seamless-scroll-container {
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  scrollbar-width: none; 
+  -ms-overflow-style: none; 
+}
+.seamless-scroll-container::-webkit-scrollbar {
+  display: none;
+}
+</style>

+ 8 - 1
src/components/ui/TechTable.vue

@@ -71,7 +71,14 @@ export default {
     min-height: 0;
     min-height: 0;
     min-width: 0;
     min-width: 0;
     width: 100%;
     width: 100%;
-    overflow-y: auto;
+    overflow: hidden;
+    /* overflow-y: auto; */
+    scrollbar-width: none; /* 兼容 Firefox 隐藏滚动条 */
+    -ms-overflow-style: none; /* 兼容 IE/Edge 隐藏滚动条 */
+}
+
+.tech-table-wrapper::-webkit-scrollbar {
+  display: none; /* 兼容 Chrome/Safari/Edge 隐藏滚动条 */
 }
 }
 
 
 .tech-table {
 .tech-table {

+ 67 - 84
src/views/Home.vue

@@ -11,38 +11,27 @@
 
 
     <!-- 地图 -->
     <!-- 地图 -->
     <template #map>
     <template #map>
-        <TongzhouTrafficMap
-          ref="trafficMapRef"
-          amapKey="db2da7e3e248c3b2077d53fc809be63f"
-          securityJsCode="a7413c674852c5eaf01d90813c5b7ef6"
-        />
+      <TongzhouTrafficMap ref="trafficMapRef" amapKey="db2da7e3e248c3b2077d53fc809be63f"
+        securityJsCode="a7413c674852c5eaf01d90813c5b7ef6" />
     </template>
     </template>
 
 
     <template #left>
     <template #left>
       <div class="panel-list">
       <div class="panel-list">
         <div class="panel-item">
         <div class="panel-item">
           <PanelContainer title="在线状态">
           <PanelContainer title="在线状态">
-  
+
             <OnlineStatusTabs />
             <OnlineStatusTabs />
-  
+
           </PanelContainer>
           </PanelContainer>
         </div>
         </div>
         <div class="panel-item">
         <div class="panel-item">
           <PanelContainer title="控制模式">
           <PanelContainer title="控制模式">
-            <TickDonutChart 
-              :chartData="controlInfoData"
-              centerTitle="650个"
-              centerSubTitle="控制信息"
-            />
+            <TickDonutChart :chartData="controlInfoData" centerTitle="650个" centerSubTitle="控制信息" />
           </PanelContainer>
           </PanelContainer>
         </div>
         </div>
         <div class="panel-item">
         <div class="panel-item">
           <PanelContainer title="故障报警">
           <PanelContainer title="故障报警">
-            <AlarmMessageList 
-              :listData="alarmData" 
-              @ignore="onAlarmIgnore" 
-              @view="onAlarmView" 
-            />
+            <AlarmMessageList :listData="alarmData" @ignore="onAlarmIgnore" @view="onAlarmView" />
           </PanelContainer>
           </PanelContainer>
         </div>
         </div>
       </div>
       </div>
@@ -52,54 +41,56 @@
       <div class="panel-list">
       <div class="panel-list">
         <div class="panel-item">
         <div class="panel-item">
           <PanelContainer title="设备状态">
           <PanelContainer title="设备状态">
-  
+
             <DeviceStatusTabs />
             <DeviceStatusTabs />
-  
+
           </PanelContainer>
           </PanelContainer>
         </div>
         </div>
         <div class="panel-item">
         <div class="panel-item">
           <PanelContainer title="勤务执行" class="table-panel">
           <PanelContainer title="勤务执行" class="table-panel">
-            <TechTable :columns="tableColumns" :data="tableData" height="263px" @mouseenter.native="pauseDutyScroll" @mouseleave.native="resumeDutyScroll">
-      
-              <template #level="{ row }">
-                <span :title="row.level" :style="{ color: row.level === '二级' ? '#FFDF0C' : '#F00' }">
-                  {{ row.level }}
-                </span>
-              </template>
 
 
-              <template #status="{ row }">
-                <span :title="row.status" :style="{ color: row.status === '进行中' ? '#FFDF0C' : '#F00' }">
-                  {{ row.status }}
-                </span>
-              </template>
+            <SeamlessScroll :data="tableData" :limit="4">
+
+              <template #default="{ list }">
+
+                <TechTable ref="dutyTable" :columns="tableColumns" :data="list" >
+
+                  <template #level="{ row }">
+                    <span :title="row.level" :style="{ color: row.level === '二级' ? '#FFDF0C' : '#F00' }">
+                      {{ row.level }}
+                    </span>
+                  </template>
+
+                  <template #status="{ row }">
+                    <span :title="row.status" :style="{ color: row.status === '进行中' ? '#FFDF0C' : '#F00' }">
+                      {{ row.status }}
+                    </span>
+                  </template>
+
+                  <template #action="{ row }">
+                    <span class="action-btn" @click="handleView(row)">
+                      查看
+                    </span>
+                  </template>
+
+                </TechTable>
 
 
-              <template #action="{ row }">
-                <span 
-                  class="action-btn" 
-                  @click="handleView(row)"
-                >
-                  查看
-                </span>
               </template>
               </template>
 
 
-            </TechTable>
+            </SeamlessScroll>
           </PanelContainer>
           </PanelContainer>
         </div>
         </div>
         <div class="panel-item">
         <div class="panel-item">
           <PanelContainer title="关键路口" class="table-panel">
           <PanelContainer title="关键路口" class="table-panel">
-            <TechTable 
-              :columns="keyIntersectionColumns" 
-              :data="keyIntersectionData"
-              height="263px"
-              @row-click="onIntersectionRowClick"
-            />
+            <TechTable :columns="keyIntersectionColumns" :data="keyIntersectionData" height="263px"
+              @row-click="onIntersectionRowClick" />
           </PanelContainer>
           </PanelContainer>
         </div>
         </div>
       </div>
       </div>
     </template>
     </template>
 
 
     <template #center>
     <template #center>
-      
+
     </template>
     </template>
 
 
   </DashboardLayout>
   </DashboardLayout>
@@ -116,6 +107,7 @@ import TechTable from '@/components/ui/TechTable.vue';
 import TongzhouTrafficMap from '@/components/TongzhouTrafficMap.vue';
 import TongzhouTrafficMap from '@/components/TongzhouTrafficMap.vue';
 import OnlineStatusTabs from '@/components/ui/OnlineStatusTabs.vue';
 import OnlineStatusTabs from '@/components/ui/OnlineStatusTabs.vue';
 import DeviceStatusTabs from '@/components/ui/DeviceStatusTabs.vue';
 import DeviceStatusTabs from '@/components/ui/DeviceStatusTabs.vue';
+import SeamlessScroll from '@/components/ui/SeamlessScroll.vue';
 
 
 
 
 export default {
 export default {
@@ -130,19 +122,23 @@ export default {
     TechTable,
     TechTable,
     TongzhouTrafficMap,
     TongzhouTrafficMap,
     OnlineStatusTabs,
     OnlineStatusTabs,
-    DeviceStatusTabs
+    DeviceStatusTabs,
+    SeamlessScroll
   },
   },
   data() {
   data() {
     return {
     return {
       dutyScrollTimer: null,
       dutyScrollTimer: null,
+      currentScrollTop: 0,   // 记录当前的精确滚动像素
+      isDataDoubled: false,  // 记录数据是否已经克隆翻倍
+      scrollContainer: null, // 存放表格内部滚动容器的 DOM
       // 在线状态面板
       // 在线状态面板
       controlInfoData: [
       controlInfoData: [
         { name: '定周期控制', value: 400, color: '#33a3ff' }, // 蓝色
         { name: '定周期控制', value: 400, color: '#33a3ff' }, // 蓝色
-        { name: '感应控制',   value: 50,  color: '#e6734d' }, // 橙色
-        { name: '干线协调',   value: 200, color: '#10b981' }, // 绿色
-        { name: '黄闪控制',   value: 6,   color: '#eab308' }, // 黄色
+        { name: '感应控制', value: 50, color: '#e6734d' }, // 橙色
+        { name: '干线协调', value: 200, color: '#10b981' }, // 绿色
+        { name: '黄闪控制', value: 6, color: '#eab308' }, // 黄色
         // { name: '关灯控制',   value: null,color: '#64748b' }, // 灰色 (没有值传入null即可隐藏数字)
         // { name: '关灯控制',   value: null,color: '#64748b' }, // 灰色 (没有值传入null即可隐藏数字)
-        { name: '自适应控制', value: 10,  color: '#2dd4bf' }, // 青色
+        { name: '自适应控制', value: 10, color: '#2dd4bf' }, // 青色
         // { name: '中心控制',   value: null,color: '#8b5cf6' }, // 紫色
         // { name: '中心控制',   value: null,color: '#8b5cf6' }, // 紫色
         // { name: '全红控制',   value: null,color: '#f43f5e' }  // 红色
         // { name: '全红控制',   value: null,color: '#f43f5e' }  // 红色
       ],
       ],
@@ -206,19 +202,17 @@ export default {
       // 搜索数据
       // 搜索数据
       currentMapSearch: 'all',
       currentMapSearch: 'all',
       mapSearchOptions: [
       mapSearchOptions: [
-        {label: '全部', value: 'all' },
-        {label: '选项2', value: '1' },
-        {label: '选项3', value: '2' },
+        { label: '全部', value: 'all' },
+        { label: '选项2', value: '1' },
+        { label: '选项3', value: '2' },
       ]
       ]
     };
     };
   },
   },
   mounted() {
   mounted() {
-    // 组件挂载时启动自动滚动
-    this.startDutyScroll();
+    
   },
   },
   beforeDestroy() {
   beforeDestroy() {
-    // 离开页面时务必销毁定时器,防止内存泄漏
-    this.pauseDutyScroll();
+  
   },
   },
   methods: {
   methods: {
     // 处理忽略逻辑
     // 处理忽略逻辑
@@ -232,14 +226,14 @@ export default {
       console.log('点击了查看:', item);
       console.log('点击了查看:', item);
       // 临时逻辑,有真实接口后可以删除
       // 临时逻辑,有真实接口后可以删除
       const position = localStorage.getItem(`pos${index + 1}`).split(',');
       const position = localStorage.getItem(`pos${index + 1}`).split(',');
-      
+
       // 地图联动
       // 地图联动
       this.$refs.trafficMapRef.focusByLocation([Number(position[0]), Number(position[1])]);
       this.$refs.trafficMapRef.focusByLocation([Number(position[0]), Number(position[1])]);
-      
+
     },
     },
     onIntersectionRowClick({ row, index }) {
     onIntersectionRowClick({ row, index }) {
       console.log(`准备跳转查看关键路口详情,当前路口:`, row.id, row.intersection);
       console.log(`准备跳转查看关键路口详情,当前路口:`, row.id, row.intersection);
-      
+
       // 使用 Vue Router 跳转,将信息通过 URL 参数 (query) 带过去
       // 使用 Vue Router 跳转,将信息通过 URL 参数 (query) 带过去
       // 注意:这里的 path 请替换为你项目中“状态监控”页面的真实路由路径
       // 注意:这里的 path 请替换为你项目中“状态监控”页面的真实路由路径
       this.$router.push({
       this.$router.push({
@@ -255,32 +249,10 @@ export default {
     handleSearch() {
     handleSearch() {
       console.log('搜索', this.currentMapSearch);
       console.log('搜索', this.currentMapSearch);
     },
     },
-    // 勤务执行自动轮播逻辑
-    startDutyScroll() {
-      // 如果数据较少不需要滚动
-      if (this.tableData.length <= 4) return;
-      this.pauseDutyScroll(); // 开启前先清除旧的,防止防抖问题
-      this.dutyScrollTimer = setInterval(() => {
-        // 将第一条数据切下来,放到数组最后,实现无限循环
-        if (this.tableData.length > 0) {
-          const firstItem = this.tableData.shift();
-          this.tableData.push(firstItem);
-        }
-      }, 2500); // 每 2.5 秒滚动一次,时间可根据需要调整
-    },
-    pauseDutyScroll() {
-      if (this.dutyScrollTimer) {
-        clearInterval(this.dutyScrollTimer);
-        this.dutyScrollTimer = null;
-      }
-    },
-    resumeDutyScroll() {
-      this.startDutyScroll();
-    },
     // 跳转逻辑修改
     // 跳转逻辑修改
     handleView(row) {
     handleView(row) {
       console.log('准备跳转查看特勤线路,当前数据:', row);
       console.log('准备跳转查看特勤线路,当前数据:', row);
-      
+
       // 使用 Vue Router 跳转,将信息通过 URL 参数 (query) 带过去
       // 使用 Vue Router 跳转,将信息通过 URL 参数 (query) 带过去
       // 注意:这里的 path 请替换为你项目中“状态监控”页面的真实路由路径
       // 注意:这里的 path 请替换为你项目中“状态监控”页面的真实路由路径
       this.$router.push({
       this.$router.push({
@@ -301,27 +273,33 @@ export default {
   flex-direction: column;
   flex-direction: column;
   gap: 16px;
   gap: 16px;
 }
 }
+
 .panel-item {
 .panel-item {
   height: 254px;
   height: 254px;
 }
 }
+
 .table-panel ::v-deep .panel-content {
 .table-panel ::v-deep .panel-content {
   padding: 0;
   padding: 0;
 }
 }
+
 .action-btn {
 .action-btn {
   color: #c4d7f0;
   color: #c4d7f0;
   cursor: pointer;
   cursor: pointer;
   transition: color 0.3s;
   transition: color 0.3s;
   user-select: none;
   user-select: none;
 }
 }
+
 .action-btn:hover {
 .action-btn:hover {
   color: #32F6F8;
   color: #32F6F8;
   text-decoration: underline;
   text-decoration: underline;
 }
 }
+
 .map-legend-pos {
 .map-legend-pos {
   position: absolute;
   position: absolute;
   bottom: 100px;
   bottom: 100px;
   right: 0;
   right: 0;
 }
 }
+
 .top-search-pos {
 .top-search-pos {
   position: absolute;
   position: absolute;
   top: 0;
   top: 0;
@@ -330,4 +308,9 @@ export default {
   flex-direction: row;
   flex-direction: row;
   column-gap: 9px;
   column-gap: 9px;
 }
 }
+.table-panel ::v-deep .tech-table-wrapper {
+  height: auto !important;
+  max-height: none !important;
+  overflow: visible !important;
+}
 </style>
 </style>

+ 1 - 1
src/views/StatusMonitoring.vue

@@ -319,7 +319,7 @@ export default {
             } else if (this.activeLeftTab === 'trunkLine') { // 干线
             } else if (this.activeLeftTab === 'trunkLine') { // 干线
                 // TODO: 干线Tab的顶部图表
                 // TODO: 干线Tab的顶部图表
             } else if (this.activeLeftTab === 'specialDuty') { // 特勤
             } else if (this.activeLeftTab === 'specialDuty') { // 特勤
-                this.openDutyDetailDialog({id: 'route_' + new Date().getTime(), label: '特勤路口'});
+                // this.openDutyDetailDialog({id: 'route_' + new Date().getTime(), label: '特勤路口'});
             }
             }
         },
         },
         // 显示总览弹窗组
         // 显示总览弹窗组