Kaynağa Gözat

新增SecurityTaskTable表格组件;新增SecurityRoutePanelSwitch路口监控组件;完成特勤监控的页面制作;

画安 1 hafta önce
ebeveyn
işleme
6b7f349272

+ 376 - 0
src/components/ui/SecurityRoutePanelSwitch.vue

@@ -0,0 +1,376 @@
+<template>
+  <div class="security-route-panel">
+    
+    <div class="panel-header">
+      <div class="header-left">
+        <span class="status-dot"></span>
+        <span class="title-text">特勤安保路线</span>
+        <span class="status-text text-danger">未开始</span>
+        <span class="level-text text-danger">一级</span>
+        <button class="btn-start">立即开始</button>
+      </div>
+    </div>
+
+    <div class="carousel-wrapper">
+      
+      <div 
+        class="nav-arrow left-arrow" 
+        :class="{ 'is-disabled': !canScrollLeft, 'is-active': canScrollLeft }" 
+        @click="handlePrev"
+      >
+        <img v-if="canScrollLeft" src="@/assets/main/main-right.png" class="arrow-img left-facing" />
+        <img v-else src="@/assets/main/main-left.png" class="arrow-img" />
+      </div>
+
+      <div class="route-list-window">
+        <div class="route-list-track">
+          
+          <div class="route-card" v-for="route in visibleRoutes" :key="route.id">
+            
+            <div class="card-top-video">
+              <video class="responsive-video" :src="route.mainVideo" autoplay loop muted></video>
+            </div>
+
+            <div class="card-bottom-content">
+              <div class="route-name">
+                <span class="blue-dot"></span>
+                {{ route.name }}
+              </div>
+              
+              <div class="map-and-info">
+                <div class="map-container">
+                  <IntersectionMap 
+                    :mapData="intersectionData" 
+                    :videoUrls="route.cornerVideos" 
+                  />
+                </div>
+                
+                <div class="info-action-box">
+                  <div class="info-list">
+                    <span>等级:<span class="text-green">{{ route.level }}</span></span>
+                    <span>方式:<span class="text-green">{{ route.mode }}</span></span>
+                    <span>时间:<span class="text-green">{{ route.time }}</span></span>
+                  </div>
+                  <button class="btn-unlock">立即解锁</button>
+                </div>
+              </div>
+            </div>
+
+          </div>
+
+        </div>
+      </div>
+
+      <div 
+        class="nav-arrow right-arrow" 
+        :class="{ 'is-disabled': !canScrollRight, 'is-active': canScrollRight }" 
+        @click="handleNext"
+      >
+        <img v-if="canScrollRight" src="@/assets/main/main-right.png" class="arrow-img" />
+        <img v-else src="@/assets/main/main-left.png" class="arrow-img left-facing" />
+      </div>
+
+    </div>
+  </div>
+</template>
+
+<script>
+import IntersectionMap from '@/components/ui/IntersectionMapVideos.vue';
+import { getIntersectionData } from '@/mock/data';
+
+export default {
+  name: 'SecurityRoutePanelSwitch',
+  components: {
+    IntersectionMap,
+  },
+  data() {
+    return {
+      intersectionData: {}, 
+      
+      currentIndex: 0, 
+      pageSize: 3, 
+      
+      routeList: [
+        { 
+          id: 1, name: '靖远路与北公路交叉口 1', level: '一级', mode: '快进', time: '30s',
+          mainVideo: require('@/assets/videos/video1.mp4'),
+          cornerVideos: { nw: require('@/assets/videos/video1.mp4'), ne: require('@/assets/videos/video2.mp4'), sw: require('@/assets/videos/video2.mp4'), se: require('@/assets/videos/video1.mp4') }
+        },
+        { 
+          id: 2, name: '靖远路与北公路交叉口 2', level: '一级', mode: '快进', time: '30s',
+          mainVideo: require('@/assets/videos/video1.mp4'),
+          cornerVideos: { nw: require('@/assets/videos/video1.mp4'), ne: require('@/assets/videos/video2.mp4'), sw: require('@/assets/videos/video2.mp4'), se: require('@/assets/videos/video1.mp4') }
+        },
+        { 
+          id: 3, name: '靖远路与北公路交叉口 3', level: '一级', mode: '快进', time: '30s',
+          mainVideo: require('@/assets/videos/video1.mp4'),
+          cornerVideos: { nw: require('@/assets/videos/video1.mp4'), ne: require('@/assets/videos/video2.mp4'), sw: require('@/assets/videos/video2.mp4'), se: require('@/assets/videos/video1.mp4') }
+        },
+        { 
+          id: 4, name: '靖远路与北公路交叉口 4', level: '一级', mode: '快进', time: '30s',
+          mainVideo: require('@/assets/videos/video1.mp4'),
+          cornerVideos: { nw: require('@/assets/videos/video1.mp4'), ne: require('@/assets/videos/video2.mp4'), sw: require('@/assets/videos/video2.mp4'), se: require('@/assets/videos/video1.mp4') }
+        },
+        { 
+          id: 5, name: '靖远路与北公路交叉口 5', level: '一级', mode: '快进', time: '30s',
+          mainVideo: require('@/assets/videos/video1.mp4'),
+          cornerVideos: { nw: require('@/assets/videos/video1.mp4'), ne: require('@/assets/videos/video2.mp4'), sw: require('@/assets/videos/video2.mp4'), se: require('@/assets/videos/video1.mp4') }
+        }
+      ]
+    };
+  },
+  computed: {
+    visibleRoutes() {
+      return this.routeList.slice(this.currentIndex, this.currentIndex + this.pageSize);
+    },
+    canScrollLeft() {
+      return this.currentIndex > 0;
+    },
+    canScrollRight() {
+      return this.currentIndex < this.routeList.length - this.pageSize;
+    }
+  },
+  async mounted() {
+    this.intersectionData = await getIntersectionData(); 
+  },
+  methods: {
+    handlePrev() {
+      if (this.canScrollLeft) {
+        this.currentIndex--;
+      }
+    },
+    handleNext() {
+      if (this.canScrollRight) {
+        this.currentIndex++;
+      }
+    }
+  }
+};
+</script>
+
+<style scoped>
+/* ================== 整体面板 ================== */
+.security-route-panel {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  box-sizing: border-box;
+  padding: 5px;
+}
+
+/* ================== 顶部 Header ================== */
+.panel-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 20px;
+  padding-left: 30px; 
+}
+
+.header-left {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.status-dot {
+  width: 10px;
+  height: 10px;
+  background-color: #68e75f;
+  border-radius: 50%;
+  box-shadow: 0 0 8px rgba(104, 231, 95, 0.6);
+}
+
+.title-text {
+  color: #ffffff;
+  font-size: 16px;
+  font-weight: bold;
+}
+
+.text-danger {
+  color: #ff5252;
+  font-size: 14px;
+}
+
+.btn-start {
+  margin-left: 15px;
+  background: rgba(40, 90, 180, 0.6);
+  border: 1px solid #448aff;
+  color: #fff;
+  padding: 4px 16px;
+  border-radius: 4px;
+  cursor: pointer;
+  transition: all 0.3s;
+}
+.btn-start:hover {
+  background: rgba(40, 90, 180, 0.9);
+  box-shadow: 0 0 10px rgba(68, 138, 255, 0.5);
+}
+
+/* ================== 轮播区容器 ================== */
+.carousel-wrapper {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  gap: 15px; 
+  min-height: 0; 
+}
+
+/* ================== 图片导航按钮统一样式 ================== */
+.nav-arrow {
+  flex-shrink: 0;
+  width: 30px;
+  height: 30px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  transition: all 0.3s;
+  background: transparent;
+  border: none;
+  outline: none;
+}
+
+.arrow-img {
+  width: 100%;
+  height: 100%;
+  object-fit: contain;
+  transition: transform 0.2s ease, filter 0.3s ease;
+}
+
+.left-facing {
+  transform: rotate(180deg);
+}
+
+.nav-arrow.is-active {
+  cursor: pointer;
+}
+.nav-arrow.is-active:hover .arrow-img {
+  transform: scale(1.15); 
+  filter: drop-shadow(0 0 10px rgba(0, 229, 255, 0.8));
+}
+
+.nav-arrow.left-arrow.is-active:hover .arrow-img.left-facing {
+  transform: rotate(180deg) scale(1.15);
+}
+
+.nav-arrow.is-disabled {
+  cursor: not-allowed;
+  opacity: 0.5; 
+}
+
+/* ================== 卡片列表视窗 ================== */
+.route-list-window {
+  flex: 1;
+  height: 100%;
+  overflow: hidden;
+}
+
+.route-list-track {
+  display: flex;
+  height: 100%;
+  gap: 15px; 
+}
+
+/* 【核心修改】:删除了这里原本的 .fade-enter-active, .fade-leave-active 动画样式 */
+
+/* ================== 单个卡片样式 ================== */
+.route-card {
+  flex: 1; 
+  display: flex;
+  flex-direction: column;
+  background: rgba(22, 34, 60, 0.6);
+  border: 1px solid rgba(100, 150, 255, 0.2);
+  border-radius: 6px;
+  overflow: hidden;
+  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
+}
+
+.card-top-video {
+  width: 100%;
+  height: 55%; 
+  background: #000;
+  border-bottom: 1px solid rgba(100, 150, 255, 0.2);
+}
+
+.responsive-video {
+  width: 100%;
+  height: 100%;
+  display: block;
+  object-fit: cover; 
+}
+
+.card-bottom-content {
+  flex: 1;
+  padding: 12px;
+  display: flex;
+  flex-direction: column;
+}
+
+.route-name {
+  color: #fff;
+  font-size: 14px;
+  display: flex;
+  align-items: center;
+  margin-bottom: 12px;
+}
+
+.blue-dot {
+  width: 8px;
+  height: 8px;
+  background-color: #448aff;
+  border-radius: 50%;
+  margin-right: 8px;
+  box-shadow: 0 0 5px rgba(68, 138, 255, 0.8);
+}
+
+.map-and-info {
+  display: flex;
+  flex: 1;
+  gap: 15px;
+}
+
+.map-container {
+  width: 110px; 
+  height: 110px;
+  flex-shrink: 0;
+  border-radius: 4px;
+  overflow: hidden;
+  position: relative;
+}
+
+.info-action-box {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+}
+
+.info-list {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+  font-size: 12px;
+  color: #a0a5b0;
+}
+
+.text-green {
+  color: #48c79c;
+}
+
+.btn-unlock {
+  align-self: flex-start; 
+  background: rgba(40, 90, 180, 0.6);
+  border: 1px solid #448aff;
+  color: #fff;
+  padding: 4px 12px;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 12px;
+  transition: all 0.3s;
+}
+
+.btn-unlock:hover {
+  background: rgba(40, 90, 180, 0.9);
+  box-shadow: 0 0 8px rgba(68, 138, 255, 0.5);
+}
+</style>

+ 417 - 0
src/components/ui/SecurityRoutePanelSwitchSmall.vue

@@ -0,0 +1,417 @@
+<template>
+  <div class="security-route-panel">
+    
+    <div class="panel-header">
+      <div class="header-left">
+        <div>
+          <span class="status-dot"></span>
+          <span class="title-text">特勤安保路线</span>
+          <span class="status-text text-danger">未开始</span>
+          <span class="level-text text-danger">一级</span>
+        </div>
+        <div>
+          <span>2026.2.18执行</span>
+          <span class="close-btn" @click="$emit('close-dialog');">x</span>
+        </div>
+      </div>
+    </div>
+
+    <div class="carousel-wrapper">
+      
+      <div 
+        class="nav-arrow left-arrow" 
+        :class="{ 'is-disabled': !canScrollLeft, 'is-active': canScrollLeft }" 
+        @click="handlePrev"
+      >
+        <img v-if="canScrollLeft" src="@/assets/main/main-right.png" class="arrow-img left-facing" />
+        <img v-else src="@/assets/main/main-left.png" class="arrow-img" />
+      </div>
+
+      <div class="route-list-window">
+        <div class="route-list-track">
+          
+          <div class="route-card" v-for="route in visibleRoutes" :key="route.id">
+            
+            <div class="card-top-video">
+              <video class="responsive-video" :src="route.mainVideo" autoplay loop muted></video>
+            </div>
+
+            <div class="card-bottom-content">
+              <div class="route-name">
+                <span class="blue-dot"></span>
+                {{ route.name }}
+              </div>
+              
+              <div class="map-and-info">
+                <div class="map-container">
+                  <IntersectionMap 
+                    :mapData="intersectionData" 
+                    :videoUrls="route.cornerVideos" 
+                  />
+                </div>
+                
+                <div class="info-action-box">
+                  <div class="info-list">
+                    <span>等级:<span class="text-green">{{ route.level }}</span></span>
+                    <span>方式:<span class="text-green">{{ route.mode }}</span></span>
+                    <span>时间:<span class="text-green">{{ route.time }}</span></span>
+                  </div>
+                </div>
+              </div>
+            </div>
+
+          </div>
+
+        </div>
+      </div>
+
+      <div 
+        class="nav-arrow right-arrow" 
+        :class="{ 'is-disabled': !canScrollRight, 'is-active': canScrollRight }" 
+        @click="handleNext"
+      >
+        <img v-if="canScrollRight" src="@/assets/main/main-right.png" class="arrow-img" />
+        <img v-else src="@/assets/main/main-left.png" class="arrow-img left-facing" />
+      </div>
+
+    </div>
+  </div>
+</template>
+
+<script>
+import IntersectionMap from '@/components/ui/IntersectionMapVideos.vue';
+import { getIntersectionData } from '@/mock/data';
+
+export default {
+  name: 'SecurityRoutePanelSwitchSmall',
+  components: {
+    IntersectionMap,
+  },
+  data() {
+    return {
+      intersectionData: {}, 
+      
+      currentIndex: 0, 
+      pageSize: 3, 
+      
+      routeList: [
+        { 
+          id: 1, name: '靖远路与北公路交叉口 1', level: '一级', mode: '快进', time: '30s',
+          mainVideo: require('@/assets/videos/video1.mp4'),
+          cornerVideos: { nw: require('@/assets/videos/video1.mp4'), ne: require('@/assets/videos/video2.mp4'), sw: require('@/assets/videos/video2.mp4'), se: require('@/assets/videos/video1.mp4') }
+        },
+        { 
+          id: 2, name: '靖远路与北公路交叉口 2', level: '一级', mode: '快进', time: '30s',
+          mainVideo: require('@/assets/videos/video1.mp4'),
+          cornerVideos: { nw: require('@/assets/videos/video1.mp4'), ne: require('@/assets/videos/video2.mp4'), sw: require('@/assets/videos/video2.mp4'), se: require('@/assets/videos/video1.mp4') }
+        },
+        { 
+          id: 3, name: '靖远路与北公路交叉口 3', level: '一级', mode: '快进', time: '30s',
+          mainVideo: require('@/assets/videos/video1.mp4'),
+          cornerVideos: { nw: require('@/assets/videos/video1.mp4'), ne: require('@/assets/videos/video2.mp4'), sw: require('@/assets/videos/video2.mp4'), se: require('@/assets/videos/video1.mp4') }
+        },
+        { 
+          id: 4, name: '靖远路与北公路交叉口 4', level: '一级', mode: '快进', time: '30s',
+          mainVideo: require('@/assets/videos/video1.mp4'),
+          cornerVideos: { nw: require('@/assets/videos/video1.mp4'), ne: require('@/assets/videos/video2.mp4'), sw: require('@/assets/videos/video2.mp4'), se: require('@/assets/videos/video1.mp4') }
+        },
+        { 
+          id: 5, name: '靖远路与北公路交叉口 5', level: '一级', mode: '快进', time: '30s',
+          mainVideo: require('@/assets/videos/video1.mp4'),
+          cornerVideos: { nw: require('@/assets/videos/video1.mp4'), ne: require('@/assets/videos/video2.mp4'), sw: require('@/assets/videos/video2.mp4'), se: require('@/assets/videos/video1.mp4') }
+        }
+      ]
+    };
+  },
+  computed: {
+    visibleRoutes() {
+      return this.routeList.slice(this.currentIndex, this.currentIndex + this.pageSize);
+    },
+    canScrollLeft() {
+      return this.currentIndex > 0;
+    },
+    canScrollRight() {
+      return this.currentIndex < this.routeList.length - this.pageSize;
+    }
+  },
+  async mounted() {
+    this.intersectionData = await getIntersectionData(); 
+  },
+  methods: {
+    handlePrev() {
+      if (this.canScrollLeft) {
+        this.currentIndex--;
+      }
+    },
+    handleNext() {
+      if (this.canScrollRight) {
+        this.currentIndex++;
+      }
+    }
+  }
+};
+</script>
+
+<style scoped>
+/* ================== 整体面板 ================== */
+.security-route-panel {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  box-sizing: border-box;
+  padding: 5px;
+}
+
+/* ================== 顶部 Header ================== */
+.panel-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 20px;
+  padding-left: 30px; 
+}
+
+.header-left {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  width: 100%;
+  gap: 12px;
+  color: #fff;
+}
+.close-btn {
+  cursor: pointer;
+  color: #ffffff;
+  font-size: 16px;
+  line-height: 1;
+  font-weight: 300;
+  opacity: 0.8;
+  transition: all 0.2s;
+  margin-left: 20px;
+}
+
+.close-btn:hover {
+  opacity: 1;
+  transform: scale(1.1);
+}
+.header-left>div {
+  display: flex;
+  align-items: center;
+  width: 100%;
+  gap: 12px;
+  color: #fff;
+  flex: 1;
+}
+.header-left>div+div {
+  justify-content: flex-end;
+}
+.status-dot {
+  width: 10px;
+  height: 10px;
+  background-color: #68e75f;
+  border-radius: 50%;
+  box-shadow: 0 0 8px rgba(104, 231, 95, 0.6);
+}
+
+.title-text {
+  color: #ffffff;
+  font-size: 16px;
+  font-weight: bold;
+}
+
+.text-danger {
+  color: #ff5252;
+  font-size: 14px;
+}
+
+.btn-start {
+  margin-left: 15px;
+  background: rgba(40, 90, 180, 0.6);
+  border: 1px solid #448aff;
+  color: #fff;
+  padding: 4px 16px;
+  border-radius: 4px;
+  cursor: pointer;
+  transition: all 0.3s;
+}
+.btn-start:hover {
+  background: rgba(40, 90, 180, 0.9);
+  box-shadow: 0 0 10px rgba(68, 138, 255, 0.5);
+}
+
+/* ================== 轮播区容器 ================== */
+.carousel-wrapper {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  gap: 15px; 
+  min-height: 0; 
+}
+
+/* ================== 图片导航按钮统一样式 ================== */
+.nav-arrow {
+  flex-shrink: 0;
+  width: 30px;
+  height: 30px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  transition: all 0.3s;
+  background: transparent;
+  border: none;
+  outline: none;
+}
+
+.arrow-img {
+  width: 100%;
+  height: 100%;
+  object-fit: contain;
+  transition: transform 0.2s ease, filter 0.3s ease;
+}
+
+.left-facing {
+  transform: rotate(180deg);
+}
+
+.right-arrow {
+  position: absolute;
+  right: 0px;
+}
+.left-arrow {
+  position: absolute;
+  left: 0px;
+}
+
+.nav-arrow.is-active {
+  cursor: pointer;
+}
+.nav-arrow.is-active:hover .arrow-img {
+  transform: scale(1.15); 
+  filter: drop-shadow(0 0 10px rgba(0, 229, 255, 0.8));
+}
+
+.nav-arrow.left-arrow.is-active:hover .arrow-img.left-facing {
+  transform: rotate(180deg) scale(1.15);
+}
+
+.nav-arrow.is-disabled {
+  cursor: not-allowed;
+  opacity: 0.5; 
+}
+
+/* ================== 卡片列表视窗 ================== */
+.route-list-window {
+  flex: 1;
+  height: 100%;
+  overflow: hidden;
+}
+
+.route-list-track {
+  display: flex;
+  height: 100%;
+  gap: 15px; 
+}
+
+/* 【核心修改】:删除了这里原本的 .fade-enter-active, .fade-leave-active 动画样式 */
+
+/* ================== 单个卡片样式 ================== */
+.route-card {
+  flex: 1; 
+  display: flex;
+  flex-direction: column;
+  background: rgba(22, 34, 60, 0.6);
+  border: 1px solid rgba(100, 150, 255, 0.2);
+  border-radius: 6px;
+  overflow: hidden;
+  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
+}
+
+.card-top-video {
+  width: 100%;
+  height: 55%; 
+  background: #000;
+  border-bottom: 1px solid rgba(100, 150, 255, 0.2);
+}
+
+.responsive-video {
+  width: 100%;
+  height: 100%;
+  display: block;
+  object-fit: cover; 
+}
+
+.card-bottom-content {
+  flex: 1;
+  padding: 12px;
+  display: flex;
+  flex-direction: column;
+}
+
+.route-name {
+  color: #fff;
+  font-size: 10px;
+  display: flex;
+  align-items: center;
+  margin-bottom: 10px;
+}
+
+.blue-dot {
+  width: 8px;
+  height: 8px;
+  background-color: #448aff;
+  border-radius: 50%;
+  margin-right: 8px;
+  box-shadow: 0 0 5px rgba(68, 138, 255, 0.8);
+}
+
+.map-and-info {
+  display: flex;
+  flex: 1;
+  gap: 15px;
+}
+
+.map-container {
+  width: 50px; 
+  height: 50px;
+  flex-shrink: 0;
+  border-radius: 4px;
+  overflow: hidden;
+  position: relative;
+}
+
+.info-action-box {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+}
+
+.info-list {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+  font-size: 12px;
+  color: #a0a5b0;
+}
+
+.text-green {
+  color: #48c79c;
+}
+
+.btn-unlock {
+  align-self: flex-start; 
+  background: rgba(40, 90, 180, 0.6);
+  border: 1px solid #448aff;
+  color: #fff;
+  padding: 4px 12px;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 12px;
+  transition: all 0.3s;
+}
+
+.btn-unlock:hover {
+  background: rgba(40, 90, 180, 0.9);
+  box-shadow: 0 0 8px rgba(68, 138, 255, 0.5);
+}
+</style>

+ 170 - 0
src/components/ui/SecurityTaskTable.vue

@@ -0,0 +1,170 @@
+<template>
+  <div class="task-table-wrapper">
+    <table class="custom-table">
+      <thead>
+        <tr>
+          <th width="10%">序号</th>
+          <th width="25%">名称</th>
+          <th width="15%">执行人</th>
+          <th width="15%">等级</th>
+          <th width="20%">状态</th>
+          <th width="15%">操作</th>
+        </tr>
+      </thead>
+      
+      <tbody>
+        <tr 
+          v-for="(row, index) in tableData" 
+          :key="index"
+          :class="{'even-row': index % 2 === 1}"
+        >
+          <td>{{ index + 1 }}</td>
+          <td>{{ row.name }}</td>
+          <td>{{ row.executor }}</td>
+          <td :class="getLevelClass(row.level)">{{ row.level }}</td>
+          <td :class="getStatusClass(row.status)">{{ row.status }}</td>
+          <td>
+            <span class="action-btn" @click="handleView(row, index)">查看</span>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'SecurityTaskTable',
+  props: {
+    // 接收父组件传来的数组数据
+    tableData: {
+      type: Array,
+      default: () => [
+        // 默认提供图片里的测试数据
+        { name: '南京街路口', executor: '测试', level: '一级', status: '未开始' },
+        { name: '南京街路口', executor: '张飞', level: '一级', status: '未开始' },
+        { name: '南京街路口', executor: '张飞', level: '一级', status: '未开始' },
+        { name: '南京街路口', executor: '张飞', level: '二级', status: '进行中' },
+        { name: '南京街路口', executor: '张飞', level: '一级', status: '未开始' },
+        { name: '北京路口',   executor: '张飞', level: '一级', status: '未开始' },
+        { name: '南京街',     executor: '张飞', level: '二级', status: '进行中' },
+        { name: '南京街路口', executor: '张飞', level: '二级', status: '进行中' },
+        { name: '南京街路口', executor: '张飞', level: '二级', status: '进行中' },
+        { name: '南京街路口', executor: '张飞', level: '二级', status: '进行中' },
+      ]
+    }
+  },
+  methods: {
+    // 根据等级返回对应的 CSS 类名
+    getLevelClass(level) {
+      if (level === '一级') return 'text-danger';
+      if (level === '二级') return 'text-warning';
+      return 'text-normal';
+    },
+    
+    // 根据状态返回对应的 CSS 类名
+    getStatusClass(status) {
+      if (status === '未开始') return 'text-danger';
+      if (status === '进行中') return 'text-warning';
+      if (status === '已完成') return 'text-success';
+      return 'text-normal';
+    },
+
+    // 点击查看按钮
+    handleView(row, index) {
+      // 抛出事件给父组件,比如打开一个新的监控弹窗
+      this.$emit('view-detail', { row, index });
+    }
+  }
+};
+</script>
+
+<style scoped>
+/* ================== 容器样式 ================== */
+.task-table-wrapper {
+  width: 100%;
+  height: 100%;
+  overflow-y: auto; /* 数据过多时允许内部滚动 */
+  background: rgba(22, 34, 60, 0.6); /* 整体的暗色背景兜底 */
+  border-radius: 4px;
+}
+
+/* 隐藏滚动条让大屏更美观 (如果需要的话) */
+.task-table-wrapper::-webkit-scrollbar {
+  display: none;
+}
+
+/* ================== 表格基础重置 ================== */
+.custom-table {
+  width: 100%;
+  border-collapse: collapse; /* 合并边框,取消默认间距 */
+  text-align: center;
+  color: #e2e8f0;
+}
+
+/* ================== 表头样式 ================== */
+.custom-table thead tr {
+  background-color: rgba(30, 45, 80, 0.8); /* 表头深色底 */
+}
+
+.custom-table th {
+  color: #48c79c; /* 还原图片里的科技青/薄荷绿色 */
+  font-size: 14px;
+  font-weight: 500;
+  padding: 14px 10px;
+  letter-spacing: 1px;
+}
+
+/* ================== 表体数据行 ================== */
+.custom-table td {
+  padding: 12px 10px;
+  font-size: 14px;
+  border-bottom: 1px solid transparent; /* 预留边框位,防止 hover 时抖动 */
+}
+
+/* 奇数行背景色 (1, 3, 5) */
+.custom-table tbody tr {
+  background-color: rgba(36, 54, 95, 0.4);
+  transition: all 0.2s ease;
+}
+
+/* 偶数行背景色 (2, 4, 6) - 斑马纹 */
+.custom-table tbody tr.even-row {
+  background-color: rgba(45, 66, 115, 0.4);
+}
+
+/* 悬浮高亮效果:大屏交互必备 */
+.custom-table tbody tr:hover {
+  background-color: rgba(68, 138, 255, 0.25);
+}
+
+/* ================== 状态文字颜色映射 ================== */
+/* 红色系:一级 / 未开始 */
+.text-danger {
+  color: #ff5b5b;
+}
+
+/* 黄色系:二级 / 进行中 */
+.text-warning {
+  color: #f39c12;
+}
+
+/* 绿色系:完成 (备用) */
+.text-success {
+  color: #2ecc71;
+}
+
+/* ================== 操作按钮 ================== */
+.action-btn {
+  color: #ffffff;
+  cursor: pointer;
+  opacity: 0.8;
+  transition: opacity 0.2s, color 0.2s;
+}
+
+.action-btn:hover {
+  opacity: 1;
+  color: #00e5ff; /* 悬浮时发光蓝 */
+  text-decoration: underline;
+}
+</style>

+ 3 - 1
src/router/index.js

@@ -7,6 +7,7 @@ import Main from "@/views/Main.vue";
 import TransitionPage from "@/views/TransitionPage.vue";
 import MainWatch from "@/views/MainWatch.vue"; 
 import StatusMonitoring from "@/views/StatusMonitoring.vue";
+import SpecialSituationMonitoring from "@/views/SpecialSituationMonitoring.vue";
 
 Vue.use(Router);
 
@@ -19,6 +20,7 @@ export default new Router({
     { path: "/transition", component: TransitionPage },
     { path: "/home", component: Home },
     { path: "/main-watch", component: MainWatch },
-    { path: "/main-surve", component: StatusMonitoring }
+    { path: "/main-surve", component: StatusMonitoring },
+    { path: "/main-security", component: SpecialSituationMonitoring }
   ]
 });

+ 4 - 0
src/styles/global.css

@@ -102,6 +102,10 @@
   height: 100%;
   box-sizing: border-box;
 }
+.main-layout.special-situation-monitoring {
+  grid-template-columns: 500px 1fr 480px;
+  height: fit-content;
+}
 
 .top-center-controls {
   position: absolute;

+ 1 - 1
src/views/Main.vue

@@ -58,7 +58,7 @@ export default {
       items: [
         { key: "home", label: "首页", img: "main-home.png", route: { path: "/home" } },
         { key: "watch", label: "状态监控", img: "main-watch.png", route: { path: "/main-watch", query: { panel: "watch" } } },
-        { key: "security", label: "特勤安保", img: "main-security.png", route: { path: "/home", query: { panel: "security" } } },
+        { key: "security", label: "特勤安保", img: "main-security.png", route: { path: "/main-security", 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: "/main-surve", query: { panel: "surve" } } },
         { key: "setting", label: "系统设置", img: "main-setting.png", route: { path: "/home", query: { panel: "setting" } } },

+ 185 - 0
src/views/SpecialSituationMonitoring.vue

@@ -0,0 +1,185 @@
+<template>
+    <div class="fluid-dashboard">
+        <div id="map-container" class="map-layer"></div>
+
+        <div class="ui-layer">
+
+            <header class="top-header">
+                <div class="right-wrap">
+                    <span class="weather">{{ weather }}</span>
+                    <span class="temperature">{{ temperature }}</span>
+                    <span class="time">{{ time }}</span>
+                    <div class="date">
+                        <span>{{ week }}</span>
+                        <span>{{ date }}</span>
+                    </div>
+                </div>
+            </header>
+
+            <main class="main-layout special-situation-monitoring">
+                <!-- 左侧边栏 -->
+                <aside class="left-sidebar">
+                    <SecurityTaskTable @view-detail="handleViewDetail" />
+                </aside>
+
+                <section class="center-area">
+                    <div class="top-center-controls">
+                        <ButtonGroup @change="handleModeChange" />
+                    </div>
+
+                    <div class="float-bottom-dock">
+                        <BottomDock @change="handleDockChange" />
+                    </div>
+
+                </section>
+
+                <aside class="right-sidebar">
+                    <MapLegend style="position: absolute; right: 20px; bottom: 80px; z-index: 100;" />
+                </aside>
+
+            </main>
+            
+        </div>
+        <SmartDialog v-for="dialog in activeDialogs" :key="dialog.id" 
+            :id="dialog.id"
+            :visible.sync="dialog.visible" 
+            :title="dialog.title" 
+            :defaultWidth="dialog.width || 400"
+            :defaultHeight="dialog.height || 300" 
+            :center="dialog.center !== false" 
+            :position="dialog.position"
+            :showClose="dialog.showClose"
+            @close="handleDialogClose(dialog.id)">
+
+            <component :is="dialog.componentName" v-bind="dialog.data"  @close-dialog="handleDialogClose(dialog.id)"></component>
+        </SmartDialog>
+        
+    </div>
+</template>
+
+<script>
+import '@/styles/global.css';
+import '@/utils/rem.js';
+
+import SecurityTaskTable from '@/components/ui/SecurityTaskTable.vue';
+import ButtonGroup from '@/components/ui/ButtonGroup.vue';
+import BottomDock from '@/components/ui/BottomDock.vue';
+import SmartDialog from '@/components/ui/SmartDialog.vue';
+import SecurityRoutePanelSwitch from '@/components/ui/SecurityRoutePanelSwitch.vue';
+import SecurityRoutePanelSwitchSmall from '@/components/ui/SecurityRoutePanelSwitchSmall.vue';
+import MapLegend from '@/components/ui/MapLegend.vue';
+
+
+export default {
+    name: 'StatusMonitoring',
+    components: {
+        SecurityTaskTable,
+        ButtonGroup,
+        BottomDock,
+        SmartDialog,
+        SecurityRoutePanelSwitch,
+        SecurityRoutePanelSwitchSmall,
+        MapLegend
+    },
+    data() {
+        return {
+            weather: '☀️ 晴',
+            temperature: '32/17℃',
+            time: '10:30:05',
+            week: '周五',
+            date: '2023.08.10',
+            currentModule: '干线协调',
+            activeDialogs: [],
+        };
+    },
+    methods: {
+        handleViewDetail() {
+            console.log('父组件接收到了查看详情事件');
+            // 这里可以根据实际业务需求来决定打开哪个弹窗,加载哪个组件
+            this.testOpenSecurityRoute();
+            this.testOpenSecurityRoute2();
+        },
+        handleModeChange(val) {
+            console.log('当前切换到了模式:', val);
+
+        },
+        handleDockChange(item) {
+            console.log('父组件接收到了 Dock 切换事件:', item.label);
+            this.currentModule = item.label;
+
+            // 在这里执行你具体的业务逻辑联动
+            if (item.label === '首页') {
+                // 重置地图视角
+            } else if (item.label === '特勤安保') {
+                // 画出安保路线,弹出视频监控框
+            } else if (item.label === '系统设置') {
+                // 弹出一个全屏的设置弹窗
+            }
+        },
+        openDialog(config) {
+            // 1. 防止重复打开同一个弹窗 (根据 id 判断)
+            const existingDialog = this.activeDialogs.find(d => d.id === config.id);
+
+            if (existingDialog) {
+                // 如果已经存在,只是将其设为可见 (SmartDialog 内部会自动把它 bringToFront 置顶)
+                existingDialog.visible = true;
+                return;
+            }
+
+            // 2. 如果不存在,则 push 一个新的弹窗对象进去
+            this.activeDialogs.push({
+                id: config.id,                     // 唯一标识 (例如路口ID 'node-101')
+                title: config.title,               // 弹窗左上角标题
+                componentName: config.component,   // 要加载的内部组件名
+                visible: true,                     // 默认可见
+                width: config.width || 450,        // 自定义宽度
+                height: config.height || 300,      // 自定义高度
+                center: config.center !== false,   // 是否居中显示
+                position: config.position || null, // 自定义坐标 {x, y}
+                showClose: config.showClose !== false, // 是否显示关闭按钮
+                data: config.data || {}            // 传给内部组件的业务数据
+            });
+        },
+
+        /**
+         * 关闭弹窗的回调
+         */
+        handleDialogClose(dialogId) {
+            // 性能优化:当用户点击 ✕ 关闭弹窗时,将其从数组中彻底移除,销毁内部组件释放内存
+            this.activeDialogs = this.activeDialogs.filter(d => d.id !== dialogId);
+        },
+
+        // ================= 测试用例:模拟各种点击行为 =================
+
+        // 模拟 1:打开特勤安保路线面板
+        testOpenSecurityRoute() {
+            this.openDialog({
+                id: 'dev-security-route', // 这里的 ID 可以根据实际业务场景动态生成,例如 'dev-security-route' 代表特勤安保路线弹窗
+                title: '',
+                component: 'SecurityRoutePanelSwitch',
+                width: 1000,
+                height: 500,
+                center: true,
+                // position: { x: 400, y: 450 },
+            });
+        },
+
+        // 模拟 2:打开特勤安保路线小面板
+        testOpenSecurityRoute2() {
+            this.openDialog({
+                id: 'dev-security-route-small', // 这里的 ID 可以根据实际业务场景动态生成,例如 'dev-security-route' 代表特勤安保路线弹窗
+                title: '',
+                component: 'SecurityRoutePanelSwitchSmall',
+                width: 550,
+                height: 300,
+                center: false,
+                position: { x: 1400, y: 550 },
+            });
+        },
+
+        
+    }
+}
+</script>
+
+<style scoped></style>