Просмотр исходного кода

新增修改密码组件;新增个人中心组件;修改DashboardLayout布局添加顶部logo图片和个人中心;

画安 1 месяц назад
Родитель
Сommit
88de39322e
3 измененных файлов с 477 добавлено и 2 удалено
  1. 209 0
      src/components/ui/ChangePassword.vue
  2. 236 0
      src/components/ui/UserProfile.vue
  3. 32 2
      src/layouts/DashboardLayout.vue

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

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

+ 32 - 2
src/layouts/DashboardLayout.vue

@@ -2,9 +2,16 @@
     <div class="fluid-dashboard">
     <div class="fluid-dashboard">
         <div class="frame-top">
         <div class="frame-top">
             <div class="title">{{ title }}</div>
             <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>
         <div class="frame-left"></div>
         <div class="frame-left"></div>
-        <div class="frame-right"></div>
+        <div class="frame-right">
+        </div>
         <div class="frame-bottom"></div>
         <div class="frame-bottom"></div>
 
 
         <slot name="map"></slot>
         <slot name="map"></slot>
@@ -76,6 +83,7 @@
 import BottomDock from '@/components/ui/BottomDock.vue';
 import BottomDock from '@/components/ui/BottomDock.vue';
 import SmartDialog from '@/components/ui/SmartDialog.vue';
 import SmartDialog from '@/components/ui/SmartDialog.vue';
 import dialogManager from '@/mixins/dialogManager';
 import dialogManager from '@/mixins/dialogManager';
+import UserProfile from '@/components/ui/UserProfile.vue';
 
 
 // 注册所有可能在弹窗中使用的内容组件
 // 注册所有可能在弹窗中使用的内容组件
 import DeviceStatusPanel from '@/components/ui/DeviceStatusPanel.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 DeviceStatusTabs from '@/components/ui/DeviceStatusTabs.vue';
 import TaskMonitorHeader from '@/components/ui/TaskMonitorHeader.vue';
 import TaskMonitorHeader from '@/components/ui/TaskMonitorHeader.vue';
 import SpecialTaskMonitorPanel from '@/components/ui/SpecialTaskMonitorPanel.vue';
 import SpecialTaskMonitorPanel from '@/components/ui/SpecialTaskMonitorPanel.vue';
+import ChangePassword from '@/components/ui/ChangePassword.vue';
 
 
 export default {
 export default {
     name: 'DashboardLayout',
     name: 'DashboardLayout',
@@ -101,6 +110,7 @@ export default {
     components: {
     components: {
         BottomDock,
         BottomDock,
         SmartDialog,
         SmartDialog,
+        UserProfile,
         DeviceStatusPanel,
         DeviceStatusPanel,
         SecurityRoutePanel,
         SecurityRoutePanel,
         IntersectionMapVideos,
         IntersectionMapVideos,
@@ -116,7 +126,8 @@ export default {
         OnlineStatusTabs,
         OnlineStatusTabs,
         DeviceStatusTabs,
         DeviceStatusTabs,
         TaskMonitorHeader,
         TaskMonitorHeader,
-        SpecialTaskMonitorPanel
+        SpecialTaskMonitorPanel,
+        ChangePassword
     },
     },
     provide() {
     provide() {
         return {
         return {
@@ -298,4 +309,23 @@ export default {
 .center-area > * {
 .center-area > * {
     pointer-events: auto; 
     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>
 </style>