Explorar o código

新增下拉选项组件;新增单选按钮组件;修改路口监控弹窗布局;

画安 hai 1 semana
pai
achega
ebbc0f7ac7

BIN=BIN
src/assets/test_img1.png


+ 207 - 0
src/components/DropdownSelect.vue

@@ -0,0 +1,207 @@
+<template>
+  <div class="custom-dropdown" ref="dropdown">
+    <div 
+      class="dropdown-trigger" 
+      :class="{ 'is-open': isOpen }" 
+      @click="toggleDropdown"
+    >
+      <span class="trigger-text">{{ currentLabel }}</span>
+      <i class="arrow-icon"></i>
+    </div>
+
+    <transition name="fade">
+      <div class="dropdown-menu" v-show="isOpen">
+        <div class="menu-arrow"></div>
+        
+        <div class="menu-list">
+          <div 
+            class="menu-item" 
+            v-for="item in options" 
+            :key="item.value"
+            :class="{ 'is-active': value === item.value }"
+            @click="selectOption(item)"
+          >
+            {{ item.label }}
+          </div>
+        </div>
+      </div>
+    </transition>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'DropdownSelect',
+  // 支持 v-model
+  model: {
+    prop: 'value',
+    event: 'change'
+  },
+  props: {
+    // 当前选中的值
+    value: {
+      type: [String, Number],
+      default: ''
+    },
+    // 选项数据,例如 [{ label: '50', value: 50 }, ...]
+    options: {
+      type: Array,
+      default: () => []
+    },
+    // 如果没有匹配项时的默认提示文本
+    placeholder: {
+      type: String,
+      default: '请选择'
+    }
+  },
+  data() {
+    return {
+      isOpen: false
+    };
+  },
+  computed: {
+    // 根据当前 value 查找对应的 label 显示在按钮上
+    currentLabel() {
+      const selected = this.options.find(opt => opt.value === this.value);
+      return selected ? selected.label : this.placeholder;
+    }
+  },
+  mounted() {
+    // 监听全局点击事件,用于点击外部关闭下拉框
+    document.addEventListener('click', this.handleClickOutside);
+  },
+  beforeDestroy() {
+    // 组件销毁时移除监听,防止内存泄漏
+    document.removeEventListener('click', this.handleClickOutside);
+  },
+  methods: {
+    toggleDropdown() {
+      this.isOpen = !this.isOpen;
+    },
+    selectOption(item) {
+      if (this.value !== item.value) {
+        this.$emit('change', item.value); // 触发 v-model 更新
+        this.$emit('select', item);       // 额外提供一个 select 事件供外部使用
+      }
+      this.isOpen = false;
+    },
+    handleClickOutside(event) {
+      // 如果点击的区域不在当前组件内部,则关闭下拉框
+      if (this.$refs.dropdown && !this.$refs.dropdown.contains(event.target)) {
+        this.isOpen = false;
+      }
+    }
+  }
+};
+</script>
+
+<style scoped>
+/* 最外层容器 */
+.custom-dropdown {
+  position: relative;
+  display: inline-block;
+  user-select: none;
+}
+
+/* --- 触发器按钮 (深色边框风格) --- */
+.dropdown-trigger {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 4px 12px;
+  min-width: 80px;
+  background-color: transparent;
+  /* 还原图片中带点透明度的蓝灰色边框 */
+  border: 1px solid rgba(100, 130, 190, 0.6); 
+  color: #ffffff;
+  font-size: 14px;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+.dropdown-trigger:hover,
+.dropdown-trigger.is-open {
+  border-color: rgba(140, 180, 255, 0.9);
+}
+
+.trigger-text {
+  margin-right: 8px;
+}
+
+/* 纯 CSS 绘制的下拉箭头 */
+.arrow-icon {
+  width: 0;
+  height: 0;
+  border-left: 4px solid transparent;
+  border-right: 4px solid transparent;
+  border-top: 5px solid #ffffff;
+  transition: transform 0.3s ease;
+}
+
+/* 展开时箭头反转向上 */
+.dropdown-trigger.is-open .arrow-icon {
+  transform: rotate(180deg);
+}
+
+/* --- 下拉菜单容器 (白色气泡风格) --- */
+.dropdown-menu {
+  position: absolute;
+  /* 位于触发器正下方并居中对齐 */
+  top: calc(100% + 10px);
+  left: 50%;
+  transform: translateX(-50%);
+  min-width: 80px;
+  background-color: #ffffff;
+  border-radius: 6px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+  /* 确保菜单层级在最上面 */
+  z-index: 1000; 
+}
+
+/* 顶部的小三角指示器 */
+.menu-arrow {
+  position: absolute;
+  top: -5px; /* 向上偏移形成凸起 */
+  left: 50%;
+  transform: translateX(-50%);
+  width: 0;
+  height: 0;
+  border-left: 6px solid transparent;
+  border-right: 6px solid transparent;
+  border-bottom: 6px solid #ffffff;
+}
+
+.menu-list {
+  padding: 8px 0;
+}
+
+/* 内部选项 */
+.menu-item {
+  padding: 6px 16px;
+  font-size: 14px;
+  color: #333333;
+  text-align: center;
+  cursor: pointer;
+  transition: background-color 0.2s, color 0.2s;
+}
+
+.menu-item:hover {
+  background-color: #f0f5ff;
+  color: #4da8ff;
+}
+
+/* 选中状态 */
+.menu-item.is-active {
+  color: #4da8ff;
+  font-weight: bold;
+}
+
+/* Vue 的过渡动画效果 */
+.fade-enter-active, .fade-leave-active {
+  transition: opacity 0.2s, transform 0.2s;
+}
+.fade-enter, .fade-leave-to {
+  opacity: 0;
+  transform: translate(-50%, -5px);
+}
+</style>

+ 118 - 0
src/components/SegmentedRadio.vue

@@ -0,0 +1,118 @@
+<template>
+  <div class="segmented-radio-group">
+    <div
+      v-for="(item, index) in options"
+      :key="item.value"
+      class="radio-item"
+      :class="{ 'is-active': currentValue === item.value }"
+      @click="handleSelect(item.value)"
+    >
+      {{ item.label }}
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'SegmentedRadio',
+  // 如果你想封装成通用组件,可以通过 v-model 绑定值
+  model: {
+    prop: 'value',
+    event: 'change'
+  },
+  props: {
+    // 接收外部传入的值
+    value: {
+      type: [String, Number],
+      default: 'temp'
+    },
+    // 选项数据配置
+    options: {
+      type: Array,
+      default: () => [
+        { label: '定周期', value: 'fixed' },
+        { label: '黄闪', value: 'yellow_flash' },
+        { label: '关灯', value: 'lights_off' },
+        { label: '步进', value: 'step' },
+        { label: '系统方案', value: 'system' },
+        { label: '感应控制', value: 'sensor' },
+        { label: '临时方案', value: 'temp' } // 默认选中项
+      ]
+    }
+  },
+  data() {
+    return {
+      currentValue: this.value
+    };
+  },
+  watch: {
+    // 监听外部 v-model 变化
+    value(val) {
+      this.currentValue = val;
+    }
+  },
+  methods: {
+    handleSelect(val) {
+      if (this.currentValue !== val) {
+        this.currentValue = val;
+        // 触发 change 事件更新 v-model
+        this.$emit('change', val);
+      }
+    }
+  }
+};
+</script>
+
+<style scoped>
+/* 容器布局 */
+.segmented-radio-group {
+  display: inline-flex;
+  align-items: center;
+  width: 100%;
+}
+
+/* 默认未选中状态 */
+.radio-item {
+  position: relative;
+  padding: 6px 5px;
+  font-size: 14px;
+  color: #ffffff;
+  /* 半透明的底色 */
+  background-color: rgba(65, 115, 205, 0.2);
+  /* 基础边框颜色 */
+  border: 1px solid #3660a5;
+  cursor: pointer;
+  user-select: none;
+  transition: all 0.2s ease;
+  
+  /* 核心技巧:利用负边距让相邻的边框重叠,避免出现双倍宽度的边框 */
+  margin-left: -1px;
+  flex: 1;
+  text-align: center;
+}
+
+/* 去除第一个元素的负边距 */
+.radio-item:first-child {
+  margin-left: 0;
+}
+
+/* 鼠标悬浮状态 */
+.radio-item:hover {
+  background-color: rgba(65, 115, 205, 0.4);
+  /* 悬浮时层级稍微提高,确保边框能完整显示 */
+  z-index: 1;
+}
+
+/* 选中状态 (高亮) */
+.radio-item.is-active {
+    background: linear-gradient(180deg, rgba(15, 30, 60, 0.9) 0%, rgba(40, 80, 150, 0.9) 100%);
+  /* 更亮的边框颜色 */
+  border-color: #82a9f4;
+  /* 微微加粗字重 */
+  font-weight: bold;
+  /* 选中状态层级最高,确保高亮边框盖住相邻未选中项的普通边框 */
+  z-index: 2;
+  /* 可选:加一点内阴影让质感更好 */
+  box-shadow: inset 0 0 2px rgba(255, 255, 255, 0.3);
+}
+</style>

+ 259 - 5
src/views/MainWatch.vue

@@ -125,9 +125,9 @@
       >
         <template #header>
           <span class="title" v-if="dialog.componentName === 'TrafficTimeSpace'">{{dialog.title}}</span>
-          <div style="display: flex; align-items: center; gap: 8px;" v-else-if="dialog.componentName === 'BigIntersectionSignalMonitoring'">
-            <span style="color: #fff; font-size: 16px;">{{dialog.title}}</span>
-            <span style="color: #4da8ff; font-size: 12px;">在线</span>
+          <div class="dialog-title-2" v-else-if="dialog.componentName === 'BigIntersectionSignalMonitoring'">
+            {{dialog.title}}/自适应控制/早高峰方案/运行时段:07:00-09:00/设备:
+            <span style="color: green;">在线</span>
           </div>
           <span class="title" v-else>{{dialog.title}}</span>
         </template>
@@ -148,7 +148,65 @@
                 :node-data="dialog.nodeData" />
             </div>
             <div class="big-mointoring-right">
-              
+              <!-- 控制方式 -->
+              <div class="control-method">
+                <div class="control-label-wrap">
+                  <span class="control-label">控制方式</span>
+                  <span>手动控制</span>
+                </div>
+                <SegmentedRadio v-model="currentMethod" />
+              </div>
+              <!-- 控制方案 -->
+              <div class="control-scheme">
+                <div class="control-label-wrap">
+                  <span class="control-label">控制方案</span>
+                  <DropdownSelect 
+                    v-model="currentScheme" 
+                    :options="schemeOptions" 
+                  />
+                </div>
+                <div class="current-stage">
+                  <div class="current-stage-warp">
+                    <div>当前阶段:</div>
+                    <div v-for="item, index in currentStageList" :key="index">
+                      <img :src="require(`@/assets/${item.img}`)" alt="stage" :class="{ 'stage-img': true, 'stage-active': item.value === currentStage }" />
+                    </div>
+                  </div>
+                </div>
+                <div class="lock-time" ref="lockTime">
+                  <div class="lock-time-label-wrap glow-header">
+                    <div class="lock-time-label">锁定时间</div>
+                    <div class="lock-time-close" @click="closeLocktime()">x</div>
+                  </div>
+                  <div class="lock-time-options">
+                    <div class="lock-time-option">
+                      <label>
+                        <input type="radio" name="lock-time" value=""/> 持续放行
+                      </label>
+                    </div>
+                    <div class="lock-time-option">
+                      <label>
+                        <input type="radio" name="lock-time" value="" /> 放行
+                        <DropdownSelect
+                          placeholder="锁定时间"
+                          v-model="currentLocktime" 
+                          :options="locktimeOptions" 
+                          @click.native.prevent
+                        />
+                        秒解锁
+                      </label>
+                    </div>
+                  </div>
+                </div>
+
+              </div>
+              <!-- 按钮组 -->
+              <div class="button-group">
+                <div>
+                  <button class="btn btn-cancel" @click="onCancel()">取消</button>
+                  <button class="btn btn-confirm" @click="onConfirm()">确认</button>
+                </div>
+              </div>
             </div>
           </div>
         </template>
@@ -166,6 +224,9 @@ import MenuItem from '@/components/MenuItem.vue';
 import SmartDialog from '@/components/SmartDialog.vue';
 import TrafficTimeSpace from '@/components/TrafficTimeSpace.vue';
 import IntersectionSignalMonitoring from '@/components/IntersectionSignalMonitoring.vue';
+import SegmentedRadio from '@/components/SegmentedRadio.vue';
+import DropdownSelect from '@/components/DropdownSelect.vue';
+
 import { menuData, makeTrafficTimeSpaceData} from '@/mock/data';
 
 export default {
@@ -175,6 +236,8 @@ export default {
     SmartDialog,
     TrafficTimeSpace,
     IntersectionSignalMonitoring,
+    SegmentedRadio,
+    DropdownSelect,
   },
   data() {
     return {
@@ -215,7 +278,28 @@ export default {
       isLoading: false,
       menuData: [], // 当前展示的树形数据
       // 弹窗相关数据
-      activeDialogs: []
+      activeDialogs: [],
+      // 控制方式数据
+      currentMethod: 'temp',
+      currentScheme: 'early_peak',
+      schemeOptions: [
+        { label: '早高峰', value: 'early_peak' },
+        { label: '晚高峰', value: 'evening_peak' },
+        { label: '平峰', value: 'normal' }
+      ],
+      currentLocktime: 0,
+      locktimeOptions: [
+        { label: '50', value: '50' },
+        { label: '100', value: '100' },
+        { label: '300', value: '300' }
+      ],
+      currentStage: '1', // 当前阶段
+      currentStageList: [
+        { value: '1', img: 'test_img1.png' },
+        { value: '2', img: 'test_img1.png' },
+        { value: '3', img: 'test_img1.png' },
+        { value: '4', img: 'test_img1.png' }
+      ]
     };
   },
   computed: {
@@ -237,6 +321,20 @@ export default {
   beforeDestroy() {
   },
   methods: {
+    onCancel() { //取消按钮
+
+    },
+    onConfirm() { //确认按钮
+
+    },
+    closeLocktime() {
+      const els = this.$refs.lockTime;
+      if (Array.isArray(els)) {
+        els.forEach(el => el.style.display = 'none');
+      } else if (els) {
+        els.style.display = 'none';
+      }
+    },
     handleMenuClick(payload) {
       console.log('父组件接收到了参数:', payload.id, payload.label);
       if (this.currentTab === 'crossing') {
@@ -1115,4 +1213,160 @@ export default {
   min-width: 0;
   min-height: 0;
 }
+.dialog-title-2{
+  font-size: calc(var(--s) * 24px);
+  letter-spacing: calc(var(--s) * 2px);
+  text-shadow: 0 0 calc(var(--s) * 10px) rgba(90, 200, 255, 0.35);
+  font-weight: 600;
+  color: #fff;
+}
+
+/** 控制方法 */
+.control-method {
+
+}
+.control-label-wrap {
+  display: flex;
+  align-items: center;
+  margin-bottom: 20px;
+  column-gap: 20px;
+}
+.control-label {
+  font-size: 24px;
+}
+.control-label-wrap span {
+  display: inline-block;
+}
+.control-method .control-label-wrap {
+  justify-content: space-between;
+}
+
+.control-scheme {
+  margin-top: 20px;
+}
+
+.lock-time {
+  width: 40%;
+  border-radius: 8px; 
+  box-shadow: 
+    inset 0px 0px 10px 0px rgba(88, 146, 255, 0.4),
+    inset 20px 0px 30px -10px rgba(88, 146, 255, 0.15);
+
+}
+.lock-time-label-wrap {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 10px;
+  border-radius: 8px 8px 0 0; 
+}
+.lock-time-label {
+  font-size: 16px;
+}
+.lock-time-options {
+  display: flex;
+  flex-direction: column;
+  row-gap: 10px;
+  font-size: 14px;
+  padding: 10px;
+}
+.lock-time-option {
+
+}
+.lock-time-close {
+  cursor: pointer;
+}
+
+.glow-header {
+  background: linear-gradient(
+    180deg, 
+    rgba(65, 115, 205, 0.6) 0%,
+    rgba(40, 70, 130, 0.1) 100%
+  );
+    
+  backdrop-filter: blur(10px);
+}
+
+.current-stage {
+  background-color: rgba(65, 115, 205, 0.2);
+  border: 1px solid #3660a5;
+  margin-bottom: 20px;
+  display: flex;
+  justify-content: center;
+}
+.current-stage-warp {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 12px 20px;
+}
+.stage-img {
+  border-radius: 5px;
+}
+.stage-active {
+  background-blend-mode: multiply; 
+  background-color: rgba(17,23,29,.5);
+}
+
+/** 按钮 */
+/* 按钮基础通用样式 */
+.btn {
+  display: inline-flex;
+  justify-content: center;
+  align-items: center;
+  height: 36px;
+  padding: 0 32px;
+  font-size: 14px;
+  border-radius: 4px;
+  cursor: pointer;
+  user-select: none;
+  transition: all 0.2s ease-in-out;
+  box-sizing: border-box;
+}
+
+/* --- 取消按钮 (幽灵按钮) --- */
+.btn-cancel {
+  background-color: transparent;
+  color: #d1d5db;
+  border: 1px solid rgba(130, 150, 190, 0.4); 
+}
+
+.btn-cancel:hover {
+  color: #ffffff;
+  border-color: rgba(130, 150, 190, 0.8);
+  background-color: rgba(255, 255, 255, 0.05);
+}
+
+.btn-cancel:active {
+  background-color: rgba(255, 255, 255, 0.1);
+}
+
+/* --- 确认按钮 (实心主按钮) --- */
+.btn-confirm {
+  background-color: #3b74ff; 
+  color: #ffffff;
+  border: 1px solid #3b74ff;
+}
+
+.btn-confirm:hover {
+  background-color: #5a8bff; 
+  border-color: #5a8bff;
+  box-shadow: 0 2px 8px rgba(59, 116, 255, 0.3);
+}
+
+.btn-confirm:active {
+  background-color: #265bed; 
+  border-color: #265bed;
+  box-shadow: none;
+}
+.button-group {
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 20px;
+}
+
+.button-group>div {
+  display: flex;
+  gap: 12px;
+}
 </style>