Procházet zdrojové kódy

Merge branch 'master' of http://121.40.40.223:3000/zizhong.wang/dtScreen

hebotao před 1 měsícem
rodič
revize
eaefdb133f

binární
src/assets/images/arrow_1.png


binární
src/assets/images/arrow_2.png


binární
src/assets/images/arrow_3.png


binární
src/assets/images/arrow_4.png


binární
src/assets/images/logo.png


+ 6 - 1
src/components/TongzhouTrafficMap.vue

@@ -238,7 +238,12 @@ export default {
         }
       });
 
-      marker.on('click', (e) => this.openLightInfo(e.target.getExtData(), e.lnglat));
+      marker.on('click', (e) => {
+        this.openLightInfo(e.target.getExtData(), e.lnglat);
+        // 抛出地图路口点击事件
+        this.$emit('map-crossing-click', e.target.getExtData(), e.lnglat);
+      });
+      
       return marker;
     },
 

+ 209 - 0
src/components/ui/ChangePassword.vue

@@ -0,0 +1,209 @@
+<template>
+  <div class="change-password-panel">
+    <form @submit.prevent="onSubmit" class="password-form">
+      
+      <div class="form-group">
+        <label>原密码</label>
+        <input 
+          type="password" 
+          v-model="form.oldPassword" 
+          placeholder="请输入原密码" 
+          required 
+          class="tech-input"
+        />
+      </div>
+
+      <div class="form-group">
+        <label>新密码</label>
+        <input 
+          type="password" 
+          v-model="form.newPassword" 
+          placeholder="请输入新密码 (至少6位)" 
+          required 
+          minlength="6"
+          class="tech-input"
+        />
+      </div>
+
+      <div class="form-group">
+        <label>确认新密码</label>
+        <input 
+          type="password" 
+          v-model="form.confirmPassword" 
+          placeholder="请再次输入新密码" 
+          required 
+          class="tech-input"
+        />
+        <span v-if="passwordMismatch" class="error-msg">两次输入的新密码不一致</span>
+      </div>
+
+      <div class="button-group">
+        <button type="button" class="btn btn-cancel" @click="onCancel">取消</button>
+        <button type="submit" class="btn btn-confirm" :disabled="passwordMismatch">确认修改</button>
+      </div>
+
+    </form>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'ChangePassword',
+  props: {
+    onSuccess: {
+      type: Function,
+      default: null
+    },
+    onCancel: {
+      type: Function,
+      default: null
+    }
+  },
+  data() {
+    return {
+      form: {
+        oldPassword: '',
+        newPassword: '',
+        confirmPassword: ''
+      }
+    }
+  },
+  computed: {
+    // 实时校验两次新密码是否一致
+    passwordMismatch() {
+      if (!this.form.newPassword || !this.form.confirmPassword) return false;
+      return this.form.newPassword !== this.form.confirmPassword;
+    }
+  },
+  methods: {
+    onSubmit() {
+      if (this.passwordMismatch) return;
+      
+      // 在这里派发事件或者调用 API
+      console.log('提交的密码表单:', this.form);
+      
+      // 直接调用传进来的 onSuccess 函数
+      if (typeof this.onSuccess === 'function') {
+        this.onSuccess(this.form);
+      }
+    },
+    handleCancel() {
+      // 直接调用传进来的 onCancel 函数
+      if (typeof this.onCancel === 'function') {
+        this.onCancel();
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.change-password-panel {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  padding: 10px;
+  box-sizing: border-box;
+}
+
+.password-form {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+  flex: 1;
+}
+
+.form-group {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  position: relative;
+}
+
+.form-group label {
+  color: #e2e8f0;
+  font-size: 14px;
+  letter-spacing: 1px;
+}
+
+/* 科技风输入框 */
+.tech-input {
+  width: 100%;
+  height: 38px;
+  background-color: rgba(0, 0, 0, 0.2);
+  border: 1px solid rgba(161, 190, 255, 0.4);
+  border-radius: 4px;
+  padding: 0 12px;
+  color: #ffffff;
+  font-size: 14px;
+  outline: none;
+  transition: all 0.3s ease;
+  box-sizing: border-box;
+}
+
+.tech-input::placeholder {
+  color: rgba(255, 255, 255, 0.3);
+}
+
+.tech-input:focus {
+  border-color: #3b74ff;
+  box-shadow: 0 0 8px rgba(59, 116, 255, 0.5);
+  background-color: rgba(0, 0, 0, 0.4);
+}
+
+.error-msg {
+  position: absolute;
+  bottom: -18px;
+  left: 0;
+  color: #ff4d4f;
+  font-size: 12px;
+}
+
+/* ================= 按钮样式 ================= */
+.button-group {
+  margin-top: auto;
+  padding-top: 20px;
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+}
+
+.btn {
+  height: 36px;
+  padding: 0 24px;
+  font-size: 14px;
+  border-radius: 4px;
+  cursor: pointer;
+  transition: all 0.2s;
+  border: none;
+  outline: none;
+}
+
+.btn:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
+.btn-cancel {
+  background-color: transparent;
+  color: #d1d5db;
+  border: 1px solid rgba(130, 150, 190, 0.4);
+}
+
+.btn-cancel:hover:not(:disabled) {
+  color: #ffffff;
+  border-color: rgba(130, 150, 190, 0.8);
+  background-color: rgba(255, 255, 255, 0.05);
+}
+
+.btn-confirm {
+  background-color: #3b74ff;
+  color: #ffffff;
+}
+
+.btn-confirm:hover:not(:disabled) {
+  background-color: #5a8bff;
+  box-shadow: 0 2px 8px rgba(59, 116, 255, 0.3);
+}
+</style>

+ 51 - 17
src/components/ui/CrossingDetailPanel.vue

@@ -54,8 +54,14 @@
                                 <div class="current-stage-warp">
                                     <div class="current-stage-label">当前阶段:</div>
                                     <div v-for="(item, index) in currentStageList" :key="index" class="stage-item-wrapper">
-                                        <img :src="require(`@/assets/${item.img}`)" alt="stage"
-                                            :class="{ 'stage-img': true, 'stage-active': item.value === currentStage }" />
+                                        <div 
+                                            class="phase-box" 
+                                            :class="{ 'is-active': item.value === currentStage }"
+                                            @click="currentStage = item.value"
+                                        >
+                                            <img :src="require(`@/assets/images/${item.img}`)" alt="stage" class="phase-image" />
+                                        </div>
+                                        
                                         <input 
                                             type="number" 
                                             v-model.number="item.time" 
@@ -208,10 +214,10 @@ export default {
             currentStage: '1', 
             // 补充了 time 属性,用于双向绑定输入框的时间
             currentStageList: [
-                { value: '1', time: 30, img: 'test_img1.png' },
-                { value: '2', time: 30, img: 'test_img1.png' },
-                { value: '3', time: 50, img: 'test_img1.png' },
-                { value: '4', time: 30, img: 'test_img1.png' }
+                { value: '1', time: 30, img: 'arrow_1.png' },
+                { value: '2', time: 30, img: 'arrow_2.png' },
+                { value: '3', time: 50, img: 'arrow_3.png' },
+                { value: '4', time: 30, img: 'arrow_4.png' }
             ]
         }
     },
@@ -532,17 +538,6 @@ export default {
     width: 100px;
 }
 
-.stage-img {
-    border-radius: 5px;
-    width: 65px;
-    height: 65px;
-}
-
-.stage-active {
-    background-blend-mode: multiply;
-    background-color: rgba(17, 23, 29, .5);
-}
-
 .stage-input {
     width: 65px;
     border: 1px solid rgba(161,190,255,0.7);
@@ -552,6 +547,45 @@ export default {
     text-align: center;
 }
 
+.phase-box {
+    position: relative;
+    width: 65px;         /* 保持原有的固定尺寸 */
+    height: 65px;
+    background: #E6F0FF; 
+    border-radius: 4px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    transition: all 0.3s ease;
+    box-sizing: border-box;
+    overflow: hidden;
+}
+
+.phase-image {
+    width: 100%;
+    height: 100%;
+    object-fit: contain;
+    display: block;
+}
+
+.phase-box::after {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background: rgba(30, 106, 255, 0.5); /* 激活时的蒙层颜色 */
+    opacity: 0; 
+    transition: opacity 0.3s ease;
+    pointer-events: none; /* 防止阻挡点击事件 */
+}
+
+.phase-box.is-active::after {
+    opacity: 1; 
+}
+
 /** 按钮 */
 /* 按钮基础通用样式 */
 .btn {

+ 30 - 10
src/components/ui/IntersectionControlCard.vue

@@ -14,7 +14,11 @@
                 <div class="info-item">驻留阶段:<span>{{ data.stage }}</span></div>
                 <div class="info-item">执行方式:<span>{{ data.mode }}</span></div>
                 <div class="info-item">剩余时间:<span class="time">{{ data.timeLeft }}s</span></div>
-                <button :class="{'btn btn-view': data.btnType === 'normal', 'action-btn primary': data.btnType === 'primary'}">{{ data.btnText }}</button>
+                <button 
+                    @click="$emit('action-click', data)"
+                    :class="{'btn btn-view margin-top-auto': data.btnType === 'normal', 'action-btn primary': data.btnType === 'primary'}">
+                    {{ data.btnText }}
+                </button>
             </div>
         </div>
 
@@ -68,20 +72,23 @@ export default {
 <style scoped>
 .control-card {
     background: #112445;
-    padding: 16px;
+    padding: 12px 16px;
     border-radius: 6px;
     border: 1px solid rgba(68, 138, 255, 0.15);
     display: flex;
     flex-direction: column;
+    height: 100%;
+    box-sizing: border-box;
 }
 
 .card-header {
     color: #fff;
     font-size: 14px;
-    margin-bottom: 16px;
+    margin-bottom: 12px;
     display: flex;
     align-items: center;
     gap: 8px;
+    flex-shrink: 0;
 }
 
 .dot {
@@ -102,16 +109,19 @@ export default {
 .card-body {
     display: flex;
     gap: 16px;
-    margin-bottom: 16px;
+    margin-bottom: 12px;
+    flex: 1;
+    min-height: 0;
 }
 
 .micro-map-container {
-    width: 140px;
-    height: 140px;
+    flex: 0 0 45%; /* 大概占据左侧 45% 的宽度 */
+    max-width: 140px;
+    height: auto;
+    aspect-ratio: 1 / 1; /* 保持正方形比例 */
     background: #050a17;
     border-radius: 4px;
     overflow: hidden;
-    flex-shrink: 0;
     border: 1px solid rgba(255, 255, 255, 0.05);
 }
 
@@ -120,6 +130,8 @@ export default {
     font-size: 13px;
     display: flex;
     flex-direction: column;
+    /* flex: 1; */
+    min-width: 0;
 }
 
 .info-item {
@@ -147,6 +159,7 @@ export default {
     font-size: 14px;
     font-weight: bold;
     transition: opacity 0.3s;
+    flex-shrink: 0; /* 防止按钮高度被压缩 */
 }
 
 .action-btn:hover {
@@ -163,12 +176,16 @@ export default {
     justify-content: space-between;
     gap: 8px;
     margin-top: auto;
+    flex-shrink: 0; /* 防止压缩 */
+    height: 20%; /* 动态高度 */
+    max-height: 56px; /* 封顶高度 */
+    min-height: 40px; /* 兜底高度 */
 }
 
 .phase-box {
     position: relative;
-    flex: 0 0 56px; 
-    height: 56px;
+    flex: 1; /* 平分剩余空间 */
+    height: 100%; /* 撑满 footer 的高 */
     background: #E6F0FF; 
     border-radius: 4px;
     display: flex;
@@ -184,7 +201,7 @@ export default {
 .phase-image {
     width: 100%;
     height: 100%;
-    object-fit: cover;
+    object-fit: contain;
     display: block;
 }
 
@@ -211,4 +228,7 @@ export default {
 .phase-box.is-active::after {
     opacity: 1; 
 }
+.margin-top-auto {
+    margin-top: auto;
+}
 </style>

+ 257 - 0
src/components/ui/MessageDialog/MessageDialog.vue

@@ -0,0 +1,257 @@
+<template>
+  <transition name="message-fade">
+    <div v-if="localVisible" class="message-overlay" @click.self="handleOverlayClick">
+      <div class="message-box" :style="zIndexStyle">
+        
+        <div class="message-header">
+          <span class="message-title">{{ title }}</span>
+          <span v-if="showClose" class="message-close" @click.stop="close">✕</span>
+        </div>
+
+        <div class="message-body">
+          <p class="message-text">{{ message }}</p>
+        </div>
+
+        <div class="message-footer" v-if="showConfirm">
+            <div class="message-btn-group">
+                <button class="message-btn" @click="confirm">{{ confirmText }}</button>
+            </div>
+        </div>
+      </div>
+    </div>
+  </transition>
+</template>
+
+<script>
+import Vue from 'vue';
+
+export default {
+  name: "MessageDialog",
+  // 将原先在 data 中的可配置参数全部移到 props 中
+  props: {
+    visible: { type: Boolean, default: false },
+    title: { type: String, default: "温馨提示" },
+    message: { type: String, default: "" },
+    type: { type: String, default: "info" }, // 'info', 'success', 'warning', 'error'
+    duration: { type: Number, default: 3000 },  // 大于0则自动关闭
+    showClose: { type: Boolean, default: true },
+    showConfirm: { type: Boolean, default: true },
+    confirmText: { type: String, default: "确认" },
+    closeOnClickModal: { type: Boolean, default: false },
+    
+    // 如果依然保留函数回调方式(主要用于命令式调用)
+    onClose: { type: Function, default: null },
+    onConfirm: { type: Function, default: null }
+  },
+  data() {
+    return {
+      // 内部变量,避免直接修改 prop(visible) 报错
+      localVisible: this.visible, 
+      zIndex: 2000, 
+      timer: null
+    };
+  },
+  computed: {
+    zIndexStyle() {
+      return {
+        zIndex: this.zIndex
+      };
+    }
+  },
+  watch: {
+    // 监听外部传入的 visible
+    visible(newVal) {
+      this.localVisible = newVal;
+      if (newVal) {
+        this.startTimer();
+      } else {
+        this.clearTimer();
+      }
+    },
+    // 监听内部 localVisible 的变化,并同步给外部组件
+    localVisible(newVal) {
+      this.$emit('update:visible', newVal);
+    }
+  },
+  mounted() {
+    if (this.localVisible) {
+      this.startTimer();
+    }
+    // 实例化时增加全局 zIndex,解决堆叠问题
+    Vue.prototype.$msgZIndex = (Vue.prototype.$msgZIndex || 2000) + 1;
+    this.zIndex = Vue.prototype.$msgZIndex;
+  },
+  methods: {
+    close() {
+      this.localVisible = false; // 触发隐藏动画
+      
+      // 触发回调和事件
+      if (typeof this.onClose === "function") {
+        this.onClose();
+      }
+      this.$emit("close");
+      this.clearTimer();
+
+      // 等待 Vue 过渡动画(0.3s)结束后,如果是命令式调用则销毁 DOM
+      setTimeout(() => {
+        // 判断:如果该组件是作为独立实例挂载在全局的(没有父组件)
+        if (this.$parent === this.$root && this.$el && this.$el.parentNode) {
+          this.$el.parentNode.removeChild(this.$el);
+          this.$destroy();
+        }
+      }, 300);
+    },
+    confirm() {
+      if (typeof this.onConfirm === "function") {
+        this.onConfirm();
+      }
+      this.$emit("confirm");
+      this.close();
+    },
+    handleOverlayClick() {
+      if (this.closeOnClickModal) {
+        this.close();
+      }
+    },
+    startTimer() {
+      this.clearTimer();
+      if (this.duration > 0) {
+        this.timer = setTimeout(() => {
+          this.close();
+        }, this.duration);
+      }
+    },
+    clearTimer() {
+      if (this.timer) {
+        clearTimeout(this.timer);
+        this.timer = null;
+      }
+    }
+  },
+  beforeDestroy() {
+    this.clearTimer();
+  }
+};
+</script>
+
+<style scoped>
+/* CSS 保持不变 */
+.message-overlay {
+  position: fixed;
+  inset: 0;
+  background: rgba(0, 5, 20, 0.6);
+  backdrop-filter: blur(8px);
+  -webkit-backdrop-filter: blur(8px);
+  z-index: 2000;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  user-select: none;
+}
+
+.message-box {
+  position: relative;
+  min-width: 320px;
+  max-width: 480px;
+  background: radial-gradient(circle at 50% 120%, rgba(20, 45, 90, 0.98) 0%, rgba(10, 20, 40, 1) 100%);
+  border: 1px solid rgba(255, 255, 255, 0.05);
+  border-radius: 12px;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  box-shadow: 
+    0 10px 40px rgba(0, 0, 0, 0.4),
+    inset 0 0 15px rgba(80, 180, 255, 0.1),
+    inset 0 1px 1px rgba(255, 255, 255, 0.2);
+}
+
+.message-header {
+  height: 40px;
+  background: rgba(30, 60, 110, 0.9);
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 0 16px;
+  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.message-title {
+  color: #ffffff;
+  font-size: 14px;
+  font-weight: 600;
+  letter-spacing: 1px;
+}
+
+.message-close {
+  cursor: pointer;
+  color: #ffffff;
+  font-size: 14px;
+  line-height: 1;
+  font-weight: 300;
+  opacity: 0.8;
+  transition: all 0.2s;
+}
+
+.message-close:hover {
+  opacity: 1;
+  transform: scale(1.1);
+}
+
+.message-body {
+  padding: 24px 16px;
+  text-align: left;
+}
+
+.message-text {
+  color: #ffffff;
+  font-size: 14px;
+  line-height: 1.6;
+  margin: 0;
+  word-break: break-word;
+}
+
+.message-footer {
+  display: flex;
+  justify-content: flex-end;
+  background: transparent;
+  position: relative;
+}
+.message-footer::after {
+    content: '';
+    width: 100%;
+    height: 1px;
+    background: #2A3B57;
+    position: absolute;
+    top: 0;
+}
+.message-btn-group {
+    padding: 16px 16px 16px 16px;
+}
+
+.message-btn {
+  padding: 6px 20px;
+  background: #1a75ff; 
+  border: none;
+  border-radius: 6px;
+  color: #ffffff;
+  font-size: 14px;
+  cursor: pointer;
+  transition: all 0.3s;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
+}
+
+.message-btn:hover {
+  background: #409EFF;
+  box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
+}
+
+.message-fade-enter-active, .message-fade-leave-active {
+  transition: opacity 0.3s, transform 0.3s;
+}
+.message-fade-enter, .message-fade-leave-to {
+  opacity: 0;
+}
+.message-fade-enter .message-box, .message-fade-leave-to .message-box {
+  transform: scale(0.85) translateY(-20px);
+}
+</style>

+ 42 - 0
src/components/ui/MessageDialog/index.js

@@ -0,0 +1,42 @@
+import Vue from 'vue';
+import MessageDialogComponent from './MessageDialog.vue';
+
+// 创建组件构造器
+const MessageConstructor = Vue.extend(MessageDialogComponent);
+
+const MessageDialog = function(options = {}) {
+  // 允许直接传入字符串作为 message
+  if (typeof options === 'string') {
+    options = { message: options };
+  }
+
+  // 实例化组件
+  const instance = new MessageConstructor({
+    propsData: options
+  });
+
+  // 挂载并插入 DOM
+  instance.$mount();
+  document.body.appendChild(instance.$el);
+  
+  // 触发显示动画
+  Vue.nextTick(() => {
+    instance.visible = true;
+  });
+
+  return instance;
+};
+
+// 封装快捷调用方法:$coolMsg.error('...')
+['success', 'warning', 'info', 'error'].forEach(type => {
+  MessageDialog[type] = options => {
+    if (typeof options === 'string') {
+      options = { message: options, type };
+    } else {
+      options.type = type;
+    }
+    return MessageDialog(options);
+  };
+});
+
+export default MessageDialog;

+ 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>

+ 71 - 12
src/components/ui/SpecialTaskMonitorPanel.vue

@@ -4,17 +4,22 @@
     <swiper class="my-swiper" :options="swiperOptions" ref="mySwiper">
       <swiper-slide v-for="(item, index) in combinedList" :key="index" class="custom-slide">
         
-        <VideoMonitorBox 
-          v-if="item.video" 
-          :videoUrl="item.video.url" 
-        />
-        <div v-else class="empty-placeholder"></div>
-
-        <IntersectionControlCard 
-          v-if="item.card" 
-          :data="item.card" 
-          class="margin-top-20"
-        />
+        <div class="top-monitor">
+          <VideoMonitorBox 
+            v-if="item.video" 
+            :videoUrl="item.video.url" 
+          />
+          <div v-else class="empty-placeholder"></div>
+        </div>
+
+        <div class="bottom-card">
+          <IntersectionControlCard 
+            v-if="item.card" 
+            :data="item.card" 
+            class="margin-top-20"
+            @action-click="handleCardAction"
+          />
+        </div>
         
       </swiper-slide>
     </swiper>
@@ -46,6 +51,10 @@ export default {
         spaceBetween: 20,      // 列间距 20px
         simulateTouch: true,   // 允许鼠标拖拽
         speed: 600,            // 滑动动画 600ms,更加优雅
+        // 开启内部监听,当 SmartDialog 缩放导致容器尺寸变化时,Swiper 自动重新计算
+        observer: true,
+        observeParents: true,
+        observeSlideChildren: true,
         navigation: {
           nextEl: '.swiper-button-next',
           prevEl: '.swiper-button-prev'
@@ -70,6 +79,38 @@ export default {
       }
       return list;
     }
+  },
+  methods: {
+    // 处理卡片按钮点击事件
+    handleCardAction(cardData) {
+      if (cardData.btnText === '立即执行') {
+        // 1. 修改文案和按钮样式状态(可选:根据你的业务,让按钮退回普通样式)
+        cardData.btnText = '立即解锁';
+        cardData.btnType = 'normal'; 
+        
+        // 2. 自动切换到下一个
+        this.slideNext();
+        
+        // 3. (可选) 这里可以顺便发送请求给后端,告知该路口已执行
+        console.log(`路口 [${cardData.name}] 已执行特勤方案`);
+        
+      } else if (cardData.btnText === '立即解锁') {
+        // 如果再次点击(已经是解锁状态),可以重置回执行状态,按需保留
+        cardData.btnText = '立即执行';
+        cardData.btnType = 'primary';
+        
+        console.log(`路口 [${cardData.name}] 已解除特勤方案`);
+      }
+    },
+    
+    // 调用 Swiper 实例滑动到下一页
+    slideNext() {
+      // 兼容不同版本的 vue-awesome-swiper 实例获取方式
+      const swiperInstance = this.$refs.mySwiper.$swiper || this.$refs.mySwiper.swiper;
+      if (swiperInstance) {
+        swiperInstance.slideNext();
+      }
+    }
   }
 }
 </script>
@@ -92,13 +133,31 @@ export default {
 .custom-slide {
   display: flex;
   flex-direction: column;
+  height: 100%;
+  box-sizing: border-box;
+}
+
+/* 分配上下比例 */
+.top-monitor {
+  height: 40%; /* 视频区域占 40% */
+  min-height: 120px; /* 防止缩太小导致完全看不见 */
+  width: 100%;
+}
+
+.bottom-card {
+  height: calc(60% - 15px); /* 卡片占 60% 减去间距 */
+  margin-top: 15px;
+  width: 100%;
+  min-height: 180px; 
 }
 
 .empty-placeholder {
-  height: 220px; /* 需与 VideoMonitorBox 的高度一致 */
+  height: 100%; 
+  width: 100%;
   background: rgba(255,255,255,0.02);
   border-radius: 6px;
   border: 1px dashed rgba(255,255,255,0.1);
+  box-sizing: border-box;
 }
 
 .margin-top-20 {

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

@@ -71,7 +71,14 @@ export default {
     min-height: 0;
     min-width: 0;
     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 {

+ 236 - 0
src/components/ui/UserProfile.vue

@@ -0,0 +1,236 @@
+<template>
+  <div class="user-profile-container" ref="profileContainer">
+    <div class="user-info" @click="toggleDropdown">
+      <img :src="avatarUrl" alt="user-avatar" class="avatar" />
+      <span class="username">{{ username }}</span>
+      <span class="arrow-icon" :class="{ 'is-open': isOpen }">▼</span>
+    </div>
+
+    <transition name="dropdown-fade">
+      <div class="dropdown-menu" v-show="isOpen">
+        <div class="dropdown-item" @click="onChangePassword">
+          <i class="icon-key"></i> 修改密码
+        </div>
+        <div class="dropdown-divider"></div>
+        <div class="dropdown-item logout" @click="onLogout">
+          <i class="icon-logout"></i> 登出
+        </div>
+      </div>
+    </transition>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'UserProfile',
+  // 注入 DashboardLayout 提供的全局弹窗管理器
+  inject: ['dialogManager'],
+  props: {
+    // 允许父组件传入用户名和头像,如果没有则使用默认值
+    username: {
+      type: String,
+      default: '今晚打老虎'
+    },
+    avatarUrl: {
+      type: String,
+      default: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png' // 默认占位头像
+    }
+  },
+  data() {
+    return {
+      isOpen: false // 控制下拉菜单显隐
+    };
+  },
+  mounted() {
+    // 监听全局点击事件,用于实现“点击空白处关闭弹窗”
+    document.addEventListener('click', this.handleClickOutside);
+  },
+  beforeDestroy() {
+    // 组件销毁前移除监听,防止内存泄漏
+    document.removeEventListener('click', this.handleClickOutside);
+  },
+  methods: {
+    toggleDropdown() {
+      this.isOpen = !this.isOpen;
+    },
+    // 点击外部关闭弹窗的逻辑
+    handleClickOutside(event) {
+      // 如果点击的区域不在当前组件的 DOM 范围内,且菜单是打开的,则关闭菜单
+      if (this.isOpen && this.$refs.profileContainer && !this.$refs.profileContainer.contains(event.target)) {
+        this.isOpen = false;
+      }
+    },
+    // 2. 直接在这里调用弹窗方法
+    onChangePassword() {
+      this.isOpen = false; // 先收起下拉菜单
+      
+      // 直接打开修改密码弹窗
+      this.dialogManager.openDialog({
+        id: 'dialog-change-password',
+        title: '修改密码',
+        component: 'ChangePassword', 
+        width: 400,
+        height: 380,
+        center: true,
+        data: {
+          // 成功回调,自动关闭弹窗
+          onSuccess: (formData) => {
+            console.log('密码已修改', formData);
+            // 这里可以写调接口的逻辑
+            this.dialogManager.closeDialog('dialog-change-password');
+          },
+          // 取消回调,自动关闭弹窗
+          onCancel: () => {
+            this.dialogManager.closeDialog('dialog-change-password');
+          }
+        }
+      });
+    },
+    onLogout() {
+      this.isOpen = false;
+      // 向父组件派发事件,由父组件执行登出接口和路由跳转
+      this.$emit('logout');
+      this.$router.push("/login");
+    }
+  }
+}
+</script>
+
+<style scoped>
+/* 容器相对定位,方便下拉菜单绝对定位 */
+.user-profile-container {
+  position: relative;
+  display: inline-block;
+}
+
+/* 顶部触发区样式 */
+.user-info {
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+  padding: 5px 10px;
+  border-radius: 4px;
+  color: #ffffff;
+  user-select: none;
+  transition: background-color 0.2s;
+}
+
+.user-info:hover {
+  background-color: rgba(255, 255, 255, 0.1);
+}
+
+.avatar {
+  width: 32px;
+  height: 32px;
+  border-radius: 50%;
+  object-fit: cover;
+  margin-right: 8px;
+  /* 增加微弱的边框,让图片在深色背景下更有层次 */
+  border: 1px solid rgba(255, 255, 255, 0.2); 
+}
+
+.username {
+  font-size: 14px;
+  margin-right: 6px;
+  letter-spacing: 0.5px;
+}
+
+.arrow-icon {
+  font-size: 10px;
+  transform: scale(0.8); /* 让自带的 ▼ 符号稍微小一点 */
+  transition: transform 0.3s ease;
+  display: inline-block;
+}
+
+/* 箭头旋转动画 */
+.arrow-icon.is-open {
+  transform: scale(0.8) rotate(180deg);
+}
+
+/* ================= 下拉菜单样式 ================= */
+.dropdown-menu {
+  position: absolute;
+  top: 100%;
+  right: 0;
+  margin-top: 10px;
+  min-width: 120px;
+  z-index: 1000;
+  overflow: hidden;
+  
+  /* 完全对齐 SmartDialog 的背景与边框风格 */
+  background: radial-gradient(circle at 20% 0%, rgba(40,120,200,0.2) 0%, rgba(20,60,130,0.4) 70%);
+  box-shadow: 
+      inset 0px 0px 10px 0px rgba(88, 146, 255, 0.4), 
+      inset 20px 0px 30px -10px rgba(88, 146, 255, 0.15),
+      0 4px 16px rgba(0, 0, 0, 0.5); /* 增加外阴影让其悬浮感更强 */
+  border: 1px solid rgba(255, 255, 255, 0.15);
+  border-radius: 8px; /* 下拉框稍微小一点的圆角 */
+  backdrop-filter: blur(8px);
+  -webkit-backdrop-filter: blur(8px);
+
+  transform-origin: top right;
+}
+
+/* 向上指的小三角(配合科技风调整颜色) */
+.dropdown-menu::before {
+  content: '';
+  position: absolute;
+  top: -6px;
+  right: 25px;
+  width: 0;
+  height: 0;
+  border-left: 6px solid transparent;
+  border-right: 6px solid transparent;
+  /* 颜色贴近顶部边框的浅蓝色 */
+  border-bottom: 6px solid rgba(88, 146, 255, 0.4); 
+}
+
+.dropdown-item {
+  padding: 12px 16px;
+  font-size: 14px;
+  color: #e2e8f0; /* 改为浅色文字 */
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: all 0.2s ease;
+}
+
+.dropdown-item:hover {
+  /* 悬浮时使用科技蓝半透明高亮 */
+  background-color: rgba(59, 116, 255, 0.3);
+  color: #ffffff; 
+}
+
+/* 登出按钮特殊处理:悬浮变红 */
+.dropdown-item.logout {
+  color: #e2e8f0;
+}
+.dropdown-item.logout:hover {
+  background-color: rgba(255, 77, 79, 0.2); /* 半透明红色警示 */
+  color: #ff4d4f; 
+}
+
+.dropdown-divider {
+  height: 1px;
+  /* 分割线颜色调整为半透明白 */
+  background-color: rgba(255, 255, 255, 0.15);
+  margin: 0;
+}
+
+/* 展开时的动画:使用 cubic-bezier 制造轻微的 Q 弹物理效果 */
+.dropdown-fade-enter-active {
+  transition: opacity 0.3s ease, transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
+}
+
+/* 收起时的动画:干脆利落,不拖泥带水 */
+.dropdown-fade-leave-active {
+  transition: opacity 0.2s ease, transform 0.2s cubic-bezier(0.4, 0, 1, 1);
+}
+
+/* 初始状态和结束状态:透明度为0,缩小至 80%,并稍微向上偏移 */
+.dropdown-fade-enter, .dropdown-fade-leave-to {
+  opacity: 0;
+  transform: scale(0.8) translateY(-10px);
+}
+</style>

+ 2 - 1
src/components/ui/VideoMonitorBox.vue

@@ -45,7 +45,8 @@ export default {
 .video-box {
   background: #112445;
   border-radius: 6px;
-  height: 220px;
+  height: 100%;
+  width: 100%;
   position: relative;
   overflow: hidden;
   border: 1px solid rgba(68, 138, 255, 0.15);

+ 32 - 2
src/layouts/DashboardLayout.vue

@@ -2,9 +2,16 @@
     <div class="fluid-dashboard">
         <div class="frame-top">
             <div class="title">{{ title }}</div>
+            <div class="top-logo">
+                <img src="@/assets/images/logo.png" />
+            </div>
+            <div class="top-right-user">
+                <UserProfile />
+            </div>
         </div>
         <div class="frame-left"></div>
-        <div class="frame-right"></div>
+        <div class="frame-right">
+        </div>
         <div class="frame-bottom"></div>
 
         <slot name="map"></slot>
@@ -76,6 +83,7 @@
 import BottomDock from '@/components/ui/BottomDock.vue';
 import SmartDialog from '@/components/ui/SmartDialog.vue';
 import dialogManager from '@/mixins/dialogManager';
+import UserProfile from '@/components/ui/UserProfile.vue';
 
 // 注册所有可能在弹窗中使用的内容组件
 import DeviceStatusPanel from '@/components/ui/DeviceStatusPanel.vue';
@@ -94,6 +102,7 @@ import OnlineStatusTabs from '@/components/ui/OnlineStatusTabs.vue';
 import DeviceStatusTabs from '@/components/ui/DeviceStatusTabs.vue';
 import TaskMonitorHeader from '@/components/ui/TaskMonitorHeader.vue';
 import SpecialTaskMonitorPanel from '@/components/ui/SpecialTaskMonitorPanel.vue';
+import ChangePassword from '@/components/ui/ChangePassword.vue';
 
 export default {
     name: 'DashboardLayout',
@@ -101,6 +110,7 @@ export default {
     components: {
         BottomDock,
         SmartDialog,
+        UserProfile,
         DeviceStatusPanel,
         SecurityRoutePanel,
         IntersectionMapVideos,
@@ -116,7 +126,8 @@ export default {
         OnlineStatusTabs,
         DeviceStatusTabs,
         TaskMonitorHeader,
-        SpecialTaskMonitorPanel
+        SpecialTaskMonitorPanel,
+        ChangePassword
     },
     provide() {
         return {
@@ -298,4 +309,23 @@ export default {
 .center-area > * {
     pointer-events: auto; 
 }
+
+.top-right-user {
+    position: absolute;
+    top: 2px;       /* 距离顶部的高度,可根据背景图微调 */
+    right: 50px;     /* 距离右侧的距离 */
+    pointer-events: auto; /* 【关键】因为 frame-top 是 none,必须在这里恢复鼠标交互,否则无法点击下拉 */
+    z-index: 100;    /* 保证层级最高,下拉菜单不被遮挡 */
+}
+.top-logo {
+    position: absolute;
+    top: 10px;
+    right: 300px;
+    height: 30px;
+}
+.top-logo img {
+    width: 100%;
+    height: 100%;
+    object-fit: contain;
+}
 </style>

+ 4 - 0
src/main.js

@@ -3,6 +3,10 @@ import App from "./App.vue";
 import router from "./router";
 import "./styles/base.css";
 import '@/utils/rem.js';
+import MessageDialog from "./components/ui/MessageDialog/index.js";
+
+// 挂载为全局方法
+Vue.prototype.$msg = MessageDialog;
 
 Vue.config.productionTip = false;
 

+ 67 - 83
src/views/Home.vue

@@ -11,38 +11,27 @@
 
     <!-- 地图 -->
     <template #map>
-        <TongzhouTrafficMap
-          ref="trafficMapRef"
-          amapKey="db2da7e3e248c3b2077d53fc809be63f"
-          securityJsCode="a7413c674852c5eaf01d90813c5b7ef6"
-        />
+      <TongzhouTrafficMap ref="trafficMapRef" amapKey="db2da7e3e248c3b2077d53fc809be63f"
+        securityJsCode="a7413c674852c5eaf01d90813c5b7ef6" />
     </template>
 
     <template #left>
       <div class="panel-list">
         <div class="panel-item">
           <PanelContainer title="在线状态">
-  
+
             <OnlineStatusTabs />
-  
+
           </PanelContainer>
         </div>
         <div class="panel-item">
           <PanelContainer title="控制模式">
-            <TickDonutChart 
-              :chartData="controlInfoData"
-              centerTitle="650个"
-              centerSubTitle="控制信息"
-            />
+            <TickDonutChart :chartData="controlInfoData" centerTitle="650个" centerSubTitle="控制信息" />
           </PanelContainer>
         </div>
         <div class="panel-item">
           <PanelContainer title="故障报警">
-            <AlarmMessageList 
-              :listData="alarmData" 
-              @ignore="onAlarmIgnore" 
-              @view="onAlarmView" 
-            />
+            <AlarmMessageList :listData="alarmData" @ignore="onAlarmIgnore" @view="onAlarmView" />
           </PanelContainer>
         </div>
       </div>
@@ -52,54 +41,56 @@
       <div class="panel-list">
         <div class="panel-item">
           <PanelContainer title="设备状态">
-  
+
             <DeviceStatusTabs />
-  
+
           </PanelContainer>
         </div>
         <div class="panel-item">
           <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>
 
-            </TechTable>
+            </SeamlessScroll>
           </PanelContainer>
         </div>
         <div class="panel-item">
           <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>
         </div>
       </div>
     </template>
 
     <template #center>
-      
+
     </template>
 
   </DashboardLayout>
@@ -116,6 +107,7 @@ import TechTable from '@/components/ui/TechTable.vue';
 import TongzhouTrafficMap from '@/components/TongzhouTrafficMap.vue';
 import OnlineStatusTabs from '@/components/ui/OnlineStatusTabs.vue';
 import DeviceStatusTabs from '@/components/ui/DeviceStatusTabs.vue';
+import SeamlessScroll from '@/components/ui/SeamlessScroll.vue';
 
 
 export default {
@@ -130,19 +122,23 @@ export default {
     TechTable,
     TongzhouTrafficMap,
     OnlineStatusTabs,
-    DeviceStatusTabs
+    DeviceStatusTabs,
+    SeamlessScroll
   },
   data() {
     return {
       dutyScrollTimer: null,
+      currentScrollTop: 0,   // 记录当前的精确滚动像素
+      isDataDoubled: false,  // 记录数据是否已经克隆翻倍
+      scrollContainer: null, // 存放表格内部滚动容器的 DOM
       // 在线状态面板
       controlInfoData: [
         { 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: 10,  color: '#2dd4bf' }, // 青色
+        { name: '自适应控制', value: 10, color: '#2dd4bf' }, // 青色
         // { name: '中心控制',   value: null,color: '#8b5cf6' }, // 紫色
         // { name: '全红控制',   value: null,color: '#f43f5e' }  // 红色
       ],
@@ -206,19 +202,17 @@ export default {
       // 搜索数据
       currentMapSearch: 'all',
       mapSearchOptions: [
-        {label: '全部', value: 'all' },
-        {label: '选项2', value: '1' },
-        {label: '选项3', value: '2' },
+        { label: '全部', value: 'all' },
+        { label: '选项2', value: '1' },
+        { label: '选项3', value: '2' },
       ]
     };
   },
   mounted() {
-    // 组件挂载时启动自动滚动
-    this.startDutyScroll();
+    
   },
   beforeDestroy() {
-    // 离开页面时务必销毁定时器,防止内存泄漏
-    this.pauseDutyScroll();
+  
   },
   methods: {
     // 处理忽略逻辑
@@ -232,13 +226,14 @@ export default {
       console.log('点击了查看:', item);
       // 临时逻辑,有真实接口后可以删除
       const position = localStorage.getItem(`pos${index + 1}`).split(',');
-      
+
       // 地图联动
       this.$refs.trafficMapRef.focusByLocation([Number(position[0]), Number(position[1])]);
+
     },
     onIntersectionRowClick({ row, index }) {
       console.log(`准备跳转查看关键路口详情,当前路口:`, row.id, row.intersection);
-      
+
       // 使用 Vue Router 跳转,将信息通过 URL 参数 (query) 带过去
       // 注意:这里的 path 请替换为你项目中“状态监控”页面的真实路由路径
       this.$router.push({
@@ -254,32 +249,10 @@ export default {
     handleSearch() {
       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) {
       console.log('准备跳转查看特勤线路,当前数据:', row);
-      
+
       // 使用 Vue Router 跳转,将信息通过 URL 参数 (query) 带过去
       // 注意:这里的 path 请替换为你项目中“状态监控”页面的真实路由路径
       this.$router.push({
@@ -300,27 +273,33 @@ export default {
   flex-direction: column;
   gap: 16px;
 }
+
 .panel-item {
   height: 254px;
 }
+
 .table-panel ::v-deep .panel-content {
   padding: 0;
 }
+
 .action-btn {
   color: #c4d7f0;
   cursor: pointer;
   transition: color 0.3s;
   user-select: none;
 }
+
 .action-btn:hover {
   color: #32F6F8;
   text-decoration: underline;
 }
+
 .map-legend-pos {
   position: absolute;
   bottom: 100px;
   right: 0;
 }
+
 .top-search-pos {
   position: absolute;
   top: 0;
@@ -329,4 +308,9 @@ export default {
   flex-direction: row;
   column-gap: 9px;
 }
+.table-panel ::v-deep .tech-table-wrapper {
+  height: auto !important;
+  max-height: none !important;
+  overflow: visible !important;
+}
 </style>

+ 7 - 1
src/views/Login.vue

@@ -117,7 +117,13 @@ export default {
         captcha: this.captchaInput
       });
 
-      if (!res.ok) return window.$toast.error(res.message);
+      if (!res.ok) {
+        this.$msg.error({
+          message: res.message,
+          duration: 0
+        });
+        return;
+      }
 
       console.log('123131');
       this.doorNavigated = false;

+ 43 - 19
src/views/StatusMonitoring.vue

@@ -11,7 +11,7 @@
         <template #map>
             <!-- 地图 -->
             <TongzhouTrafficMap amapKey="db2da7e3e248c3b2077d53fc809be63f"
-                securityJsCode="a7413c674852c5eaf01d90813c5b7ef6" />
+                securityJsCode="a7413c674852c5eaf01d90813c5b7ef6" @map-crossing-click="handleMapCrossingClick" />
         </template>
 
         <template #left>
@@ -66,7 +66,10 @@ import ButtonGroup from '@/components/ui/ButtonGroup.vue';
 import { makeTrafficTimeSpaceData } from '@/mock/data';
 import testVideo1 from '@/assets/videos/video1.mp4';
 import testVideo2 from '@/assets/videos/video2.mp4';
-import testImg1 from '@/assets/test_img1.png';
+import arrow1 from '@/assets/images/arrow_1.png';
+import arrow2 from '@/assets/images/arrow_2.png';
+import arrow3 from '@/assets/images/arrow_3.png';
+import arrow4 from '@/assets/images/arrow_4.png';
 
 
 export default {
@@ -237,6 +240,25 @@ export default {
 
     },
     methods: {
+        // 处理地图点击事件
+        handleMapCrossingClick(mapData, lnglat) {
+            console.log('父组件接收到了地图路口点击事件:', mapData, lnglat);
+            // 组装模拟数据
+            let nodeData = {
+                id: Math.random(1, 100),
+                label: mapData.road,
+            }
+            console.log(nodeData);
+            if (this.activeLeftTab === 'overview') { // 总览
+                this.showOverviewDalogs(nodeData);
+            } else if (this.activeLeftTab === 'crossing') { // 路口
+                this.showCrossingDalogs(nodeData);
+            } else if (this.activeLeftTab === 'trunkLine') { // 干线
+                this.showTrunkLineDalogs(nodeData);
+            } else if (this.activeLeftTab === 'specialDuty') { // 特勤
+                this.showSpecialDutyDalogs(nodeData);
+            }
+        },
         // 模式切换
         onViewSelect(item) {
             console.log('你点击了:', item.label);
@@ -300,7 +322,7 @@ export default {
             } else if (this.activeLeftTab === 'trunkLine') { // 干线
                 // TODO: 干线Tab的顶部图表
             } else if (this.activeLeftTab === 'specialDuty') { // 特勤
-                this.openDutyDetailDialog();
+                // this.openDutyDetailDialog({id: 'route_' + new Date().getTime(), label: '特勤路口'});
             }
         },
         // 显示总览弹窗组
@@ -454,7 +476,8 @@ export default {
         },
         // 显示特勤弹窗组
         showSpecialDutyDalogs(nodeData) {
-            console.log('显示干线弹窗组', nodeData.id, nodeData.label);
+            console.log('显示特勤弹窗组', nodeData.id, nodeData.label);
+            this.openDutyDetailDialog(nodeData);
         },
         // === 解析路由参数并执行对应操作 ===
         checkRouteParams() {
@@ -471,7 +494,7 @@ export default {
                 // 这里判断的条件改为 id
                 if (action === 'open-dialog' && id) {
                     this.$nextTick(() => {
-                        this.openDutyDetailDialog(id); // 打开特勤弹窗
+                        this.openDutyDetailDialog({id: id, label: '特勤路口'}); // 打开特勤弹窗
                     });
                 }
             }
@@ -494,14 +517,16 @@ export default {
         },
 
         // === 特勤详情弹窗 (你需要根据实际组件名替换) ===
-        async openDutyDetailDialog(dutyId) {
-            console.log('准备打开特勤线路详情,ID:', dutyId);
+        async openDutyDetailDialog(nodeData) {
+            console.log('准备打开特勤线路详情:', nodeData);
             // 1. 获取数据
             const panelData = await this.fetchSpecialTaskData();
-
+            
+            panelData.taskInfo.name = nodeData.label;
+            const id = 'special-task-dialog' + new Date().getTime();
             // 2. 呼出弹窗
             this.$refs.layout.openDialog({
-                id: 'special-task-dialog',
+                id: id,
                 title: ' ', // 留空以隐藏默认标题,使用自定义 Header
                 width: 1400, // 弹窗宽一点,容纳 3 列
                 height: 700,
@@ -512,14 +537,13 @@ export default {
                 // 挂载主体组件和数据
                 component: 'SpecialTaskMonitorPanel',
                 data: { panelData: panelData },
-
                 // 挂载自定义 Header 和数据
                 headerComponent: 'TaskMonitorHeader',
                 headerProps: {
                     taskData: panelData.taskInfo,
                     onEndTask: () => {
                         console.log('点击了结束任务');
-                        this.$refs.layout.handleDialogClose('special-task-dialog');
+                        this.$refs.layout.handleDialogClose(id);
                     }
                 }
             });
@@ -560,10 +584,10 @@ export default {
                         btnText: '立即解锁',
                         btnType: 'normal',
                         phases: [
-                            { id: 1, icon: '↑', img: testImg1, active: false },
-                            { id: 2, icon: '↰', img: testImg1, active: false },
-                            { id: 3, icon: '↑', img: testImg1, active: true }, // 当前激活相位
-                            { id: 4, icon: '↰', img: testImg1, active: false }
+                            { id: 1, icon: '↑', img: arrow1, active: false },
+                            { id: 2, icon: '↰', img: arrow2, active: false },
+                            { id: 3, icon: '↑', img: arrow3, active: true }, // 当前激活相位
+                            { id: 4, icon: '↰', img: arrow4, active: false }
                         ],
                         // 传给你原有的 IntersectionMapVideos 组件的数据
                         mapData: {
@@ -593,10 +617,10 @@ export default {
                         stage: 2, mode: '系统', timeLeft: 15,
                         btnText: '立即执行', btnType: 'primary',
                         phases: [
-                            { id: 1, icon: '↑', img: testImg1, active: true },
-                            { id: 2, icon: '↰', img: testImg1, active: false },
-                            { id: 3, icon: '↑', img: testImg1, active: false },
-                            { id: 4, icon: '↰', img: testImg1, active: false }
+                            { id: 1, icon: '↑', img: arrow1, active: true },
+                            { id: 2, icon: '↰', img: arrow2, active: false },
+                            { id: 3, icon: '↑', img: arrow3, active: false },
+                            { id: 4, icon: '↰', img: arrow4, active: false }
                         ],
                         mapData: {
                             armsConfig: {