liziliang 6 dní pred
rodič
commit
fb2365ba6b

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 9 - 0
src/assets/icons/pdm-logo.svg


+ 214 - 138
src/layout/components/header.vue

@@ -1,30 +1,38 @@
 <template>
   <div class="commonHeader">
-    <div style="width: 155px">
+    <!-- Logo区域 -->
+    <div class="logo-area" @click="gotoMain">
       <svg-icon
-        v-if="routeMeta.back"
-        icon-class="back"
-        size="48"
-        @click="commonBack"
+        icon-class="pdm-logo"
+        size="64"
+        style="height: 70px; width: 90px"
       />
-      <!-- <svg-icon v-else icon-class="LOGO" style="height: 48px; width: 155px" /> -->
+      <h2 style="margin: 0;">综合管理平台</h2>
     </div>
-    <div class="middle-title">
-      {{ routeMeta.title }}
+
+    <!-- 导航选项卡 -->
+    <div class="tab-container">
+      <div
+        v-for="tab in availableTabs"
+        :key="tab.name"
+        class="tab"
+        :class="{ active: activeTab === tab.name }"
+        @click="navigateTo(tab)"
+      >
+        {{ tab.name }}
+      </div>
     </div>
-    <div v-if="routeMeta.back"></div>
-    <div v-else>
+
+    <!-- 用户功能区 -->
+    <div class="user-area">
       <el-space>
-        <div>
-          <svg-icon
-            class="activeNotice"
-            icon-class="lingdang"
-            size="48"
-            @click="messageStatus = !messageStatus"
-          />
-        </div>
-        <div class="task"></div>
-        <div>
+        <svg-icon
+          class="activeNotice"
+          icon-class="lingdang"
+          size="48"
+          @click="messageStatus = !messageStatus"
+        />
+        <div class="user-info">
           <div class="name">{{ userStore.user.username }}</div>
           <div class="work">{{ userStore.user.station }}</div>
         </div>
@@ -33,30 +41,20 @@
           trigger="contextmenu"
           @command="handleCommand"
         >
-          <img
-            v-if="userStore.user.avatar"
-            :src="baseUrl + userStore.user.avatar"
-            fit="cover"
-            style="
-              width: 48px;
-              height: 48px;
-              border-radius: 24px;
-              border: 1px solid #ccc;
-            "
-            @click="showClick"
-          />
-          <svg-icon
-            v-else
-            icon-class="head"
-            size="48"
-            style="
-              width: 48px;
-              height: 48px;
-              border-radius: 24px;
-              border: 1px solid #ccc;
-            "
-            @click="showClick"
-          />
+          <div class="avatar-wrapper">
+            <img
+              v-if="userStore.user.avatar"
+              :src="baseUrl + userStore.user.avatar"
+              class="avatar"
+              @click="showClick"
+            />
+            <svg-icon
+              v-else
+              icon-class="head"
+              class="avatar"
+              @click="showClick"
+            />
+          </div>
           <template #dropdown>
             <el-dropdown-menu style="width: 150px">
               <el-dropdown-item command="change">修改密码</el-dropdown-item>
@@ -66,49 +64,96 @@
         </el-dropdown>
       </el-space>
     </div>
+
+    <!-- IP配置抽屉 -->
+    <el-drawer
+      v-model="drawerVisible"
+      :destroy-on-close="true"
+      title="配置IP地址"
+    >
+      <Addresss @finish="saveAddressFinish"/>
+    </el-drawer>
   </div>
 </template>
 
 <script lang="ts" setup>
-// import dayjs from "dayjs";
-import type { DropdownInstance } from "element-plus";
+import {ref, computed} from 'vue';
+import {useRouter, useRoute} from 'vue-router';
 import { logoutApi } from "@/api/auth";
 import { useUserStore } from "@/store";
+import Addresss from "@/views/sets/address.vue";
 
+const router = useRouter();
+const route = useRoute();
 const userStore = useUserStore();
 
 const baseUrl = import.meta.env.VITE_APP_UPLOAD_URL;
+const drawerVisible = ref(false);
+const messageStatus = ref(false);
+const activeTab = ref('');
+const dropdown1 = ref();
+const showClick = () => {
+  if (!dropdown1.value) return;
+  dropdown1.value.handleOpen();
+};
+// 所有可能的选项卡
+const allTabs = [
+  {name: '首页', handler: () => router.push("/main")},
+  {name: '用户管理', permission: 'canSetPermission', handler: () => router.push("/users")},
+  {name: '角色管理', permission: 'setRoles', handler: () => router.push("/roles")},
+  {name: '消息管理', permission: 'canSetMessageOrg', handler: () => router.push("/messageOrg")},
+  {name: '文件管理', permission: 'canFileShare', handler: () => router.push("/files")},
+  {name: '配置IP', permission: 'canSetIP', handler: () => openDrawer()}
+];
 
-const router = useRouter();
-const route = useRoute();
-const routeMeta = computed(() => {
-  return route.meta;
+// 根据权限过滤可用的选项卡
+const availableTabs = computed(() => {
+  return allTabs.filter(tab => {
+    // 如果没有 permission 属性,则默认显示(如首页)
+    if (!tab.permission) return true;
+    // 如果有 permission 属性,则检查用户是否有该权限
+    return userStore.user[tab.permission];
+  });
 });
 
-const dropdown1 = ref<DropdownInstance>();
+// 初始化选中状态
+const initActiveTab = () => {
+  const currentPath = route.path;
+  const matchedTab = allTabs.find(tab =>
+    currentPath.includes(tab.name.replace('管理', '').toLowerCase())
+  );
+  activeTab.value = matchedTab?.name || '';
+};
 
-const processCount = ref(50);
-const messageStatus = ref(false);
+initActiveTab();
 
-const showClick = () => {
-  if (!dropdown1.value) return;
-  dropdown1.value.handleOpen();
+// 导航方法
+const navigateTo = (tab) => {
+  activeTab.value = tab.name;
+  tab.handler();
 };
 
-const commonBack = (itemValue) => {
-  router.back();
+const openDrawer = () => {
+  drawerVisible.value = true;
 };
 
-const handleCommand = (command: string | number | object) => {
+const saveAddressFinish = () => {
+  drawerVisible.value = false;
+};
+
+const gotoMain = () => {
+  activeTab.value = '';
+  router.push("/main");
+};
+
+const handleCommand = (command: string) => {
   if (command === "b") {
     logoutApi().then(() => {
       localStorage.setItem("token", "");
-
       router.replace("/login");
       userStore.$reset();
     });
-  }
-  if (command === "change") {
+  } else if (command === "change") {
     router.push({
       name: "changePassword",
       query: {
@@ -121,98 +166,129 @@ const handleCommand = (command: string | number | object) => {
 </script>
 
 <style lang="scss" scoped>
-:deep(.el-dropdown-menu__item) {
-  height: 60px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-weight: 500;
-  font-size: 24px;
-  color: rgba(0, 0, 0, 0.9);
-}
-
 .commonHeader {
   height: $navbar-height;
   width: 100%;
-  background-color: #409eff;
+  background: linear-gradient(to bottom, #2164ee, #1C59D5);
   display: flex;
   justify-content: space-between;
   align-items: center;
   padding: 16px 24px;
-  //border: 1px solid red;
-
-  .middle-title {
-    font-weight: 500;
-    font-size: 38px;
-    color: rgba(0, 0, 0, 0.9);
-    line-height: 24px;
-    text-align: center;
-  }
 
-  .date {
-    font-weight: 500;
-    font-size: 14px;
-    color: rgba(0, 0, 0, 0.6);
-    line-height: 14px;
-    text-align: center;
-  }
+  .logo-area {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    cursor: pointer;
 
-  .time {
-    font-weight: 500;
-    font-size: 20px;
-    color: rgba(0, 0, 0, 0.9);
-    line-height: 23px;
+    h2 {
+      color: white;
+    }
   }
 
-  .head {
-    width: 48px;
-    height: 48px;
-    border-radius: 24px;
-  }
+  .tab-container {
+    display: flex;
+    gap: 12px;
+    height: 180%;
+    align-items: flex-end;
+    padding: 0 20px 2px;
+    background: linear-gradient(to bottom, #2164ee, #1C59D5);
+    overflow-x: auto;
+    scrollbar-width: none;
 
-  .name {
-    font-weight: 500;
-    font-size: 20px;
-    color: rgba(0, 0, 0, 0.9);
-    line-height: 14px;
-    text-align: right;
-  }
+    &::-webkit-scrollbar {
+      display: none;
+    }
 
-  .work {
-    font-weight: 500;
-    font-size: 14px;
-    color: rgba(0, 0, 0, 0.6);
-    line-height: 14px;
-    text-align: right;
-    margin-top: 5px;
-  }
+    &::before {
+      content: '';
+      position: absolute;
+      left: 20px;
+      right: 20px;
+      bottom: 0;
+      height: 3px;
+      background: linear-gradient(90deg,
+        rgba(255, 255, 255, 0) 0%,
+        rgba(255, 255, 255, 0.4) 50%,
+        rgba(255, 255, 255, 0) 100%);
+      z-index: 1;
+    }
+
+    .tab {
+      padding: 16px 32px;
+      background: linear-gradient(#2164ee, #1C59D5);
+      font-weight: 600;
+      font-size: 20px;
+      color: #000000;
+      cursor: pointer;
+      position: relative;
+      flex-shrink: 0;
+      min-width: max-content;
+      text-align: center;
+      white-space: nowrap;
+      margin-bottom: 6px;
+      transition: all 0.3s ease;
+
+      &:hover {
+        background: rgba(50, 140, 230, 0.7) !important;
+        backdrop-filter: blur(4px);
+        transform: translateY(-3px);
+        box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
+      }
 
-  .task {
-    padding-top: 5px;
-    margin-right: 10px;
+      &.active {
+        background: linear-gradient(to bottom, #2854ad, #133e8c);
+        color: #ffffff;
+        box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
+
+        &::after {
+          content: '';
+          position: absolute;
+          left: 0;
+          right: 0;
+          bottom: -6px;
+          height: 6px;
+          background: #ffa958;
+          z-index: 2;
+        }
+      }
+    }
   }
 
-  // .activeNotice {
-  //   animation: swing 0.15s infinite alternate ease-in-out;
-  // }
-
-  // @keyframes swing {
-  //   0% {
-  //     transform: rotate(-45deg);
-  //   }
-
-  //   100% {
-  //     transform: rotate(45deg);
-  //   }
-  // }
-
-  .process {
-    font-weight: 500;
-    font-size: 14px;
-    color: rgba(0, 0, 0, 0.6);
-    line-height: 14px;
-    text-align: right;
-    margin-top: 8px;
+  .user-area {
+    .name {
+      font-weight: 500;
+      font-size: 20px;
+      color: rgba(0, 0, 0, 0.9);
+      line-height: 14px;
+      text-align: right;
+    }
+
+    .work {
+      font-weight: 500;
+      font-size: 14px;
+      color: rgba(0, 0, 0, 0.6);
+      line-height: 14px;
+      text-align: right;
+      margin-top: 5px;
+    }
+
+    .avatar {
+      width: 48px;
+      height: 48px;
+      border-radius: 24px;
+      border: 1px solid #ccc;
+    }
   }
 }
+
+:deep(.el-dropdown-menu__item) {
+  height: 60px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-weight: 500;
+  font-size: 24px;
+  color: rgba(0, 0, 0, 0.9);
+}
 </style>

+ 10 - 1
src/store/modules/user.ts

@@ -124,7 +124,16 @@ export const useUserStore = defineStore("user", () => {
     resetToken,
     isGetAuth,
   };
-});
+  },
+  {
+    // 持久化配置
+    persist: {
+      key: "management-user",
+      storage: localStorage,
+      paths: ["user"]
+    }
+  }
+);
 
 // 非setup
 export function useUserStoreHook() {

+ 1 - 1
src/styles/index.scss

@@ -40,7 +40,7 @@
 .common-box {
   background: #ffffff;
   border-radius: 8px 8px 8px 8px;
-  padding: 25px;
+  padding: 50px;
 }
 
 //大屏相关公共样式

+ 8 - 4
src/views/main/fileList.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="common-box" style="margin-top: 20px">
+  <div class="common-box" style="margin-top: -80px">
     <TopTitle :messageType="messageType" :title="title" icon="laba"/>
     <div>
       <el-input
@@ -15,7 +15,7 @@
       </el-input>
     </div>
     <div class="list-box">
-      <el-scrollbar style="width: 100%; height: 100%">
+      <el-scrollbar style="width: 100%; height: 130%">
         <!--        <div>-->
         <!--          <div class="line">-->
         <!--            <div style="width: 100%" class="title">-->
@@ -31,7 +31,7 @@
         <!--            <el-descriptions-item label="Place">Suzhou</el-descriptions-item>-->
         <!--          </el-descriptions>-->
 
-        <el-table :data="dataList" style="width: 100%">
+        <el-table :data="dataList" style="width: 100%; height: 100%">
           <el-table-column prop="fileName" label="文件名称"/>
           <el-table-column prop="fileTitle" label="文件标题"/>
           <el-table-column label="操作">
@@ -158,7 +158,11 @@ const getDataList = () => {
   margin-top: 5px;
   height: calc(100vh - 780px);
   padding: 0 20px;
-
+  /* 防止超出父级容器 */
+  max-width: 100%; /* 宽度不超过父级 */
+  max-height: 100%; /* 高度不超过父级 */
+  overflow: hidden; /* 内容超出时滚动 */
+  box-sizing: border-box; /* padding 和 border 计入宽高 */
   .line {
     border-bottom: 1px solid #eee;
     line-height: 40px;

+ 67 - 22
src/views/main/main.vue

@@ -1,38 +1,83 @@
 <template>
-  <div>
-    <el-row :gutter="20">
-      <el-col :span="24">
-        <Systems/>
-        <el-row :gutter="20">
-          <el-col :span="20">
-            <ScreenEntry/>
-          </el-col>
-          <el-col
-            v-if="userStore.user.canSetIP || userStore.user.canSetPermission"
-            :span="4"
-          >
-            <Set/>
-          </el-col>
-        </el-row>
+  <div class="dashboard-container">
+    <!-- 第一行:主内容区(固定高度) -->
+    <el-row :gutter="20" class="fixed-height-row">
+      <el-col :span="16" class="left-panel">
+        <ScreenEntry class="fixed-content"/>
+      </el-col>
+      <el-col :span="8" class="right-panel">
+        <Systems class="fixed-content"/>
       </el-col>
-      <!--      <el-col :span="8">-->
-      <!--        <Task />-->
-      <!--      </el-col>-->
     </el-row>
-    <el-row :gutter="20">
+
+    <!-- 第二行:底部功能区 -->
+    <el-row :gutter="20" class="bottom-row">
       <el-col :span="8">
-        <FileList title="文件共享"/>
+        <FileList title="文件共享" class="content-box"/>
       </el-col>
       <el-col :span="8">
-        <Message message-type="2" title="系统公告"/>
+        <Message message-type="2" title="系统公告" class="content-box"/>
       </el-col>
       <el-col :span="8">
-        <Message message-type="0" title="个人公告"/>
+        <Message message-type="0" title="个人公告" class="content-box"/>
       </el-col>
     </el-row>
   </div>
 </template>
 
+<style scoped>
+.dashboard-container {
+  padding: 20px;
+  height: 100vh; /* 视窗高度 */
+  display: flex;
+  flex-direction: column;
+}
+
+/* 主内容区固定高度 */
+.fixed-height-row {
+  flex: 0 0 60vh; /* 固定高度占视窗60% */
+  margin-bottom: 20px;
+  display: flex;
+}
+
+.left-panel {
+  margin-top: 24px;
+  width: 66.66%; /* 2/3宽度 */
+  padding-right: 10px; /* 替代gutter效果 */
+}
+
+.right-panel {
+  margin-top: 24px;
+  width: 33.33%; /* 1/3宽度 */
+  padding-left: 10px; /* 替代gutter效果 */
+}
+
+/* 固定内容区块样式 */
+.fixed-content {
+  height: 95%;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+}
+
+/* 底部功能区自适应 */
+.bottom-row {
+  flex: 1; /* 剩余空间 */
+  min-height: 40vh; /* 最小高度 */
+}
+
+.content-box {
+  height: 100%;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  padding: 20px;
+  overflow: visible;
+}
+</style>
 <script lang="ts" setup>
 import Systems from "@/views/systems/systems.vue";
 import ScreenEntry from "@/views/main/screenEntry.vue";

+ 1 - 1
src/views/main/message.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="common-box" style="margin-top: 20px">
+  <div class="common-box" style="margin-top: -80px">
 
     <TopTitle :messageType="messageType" :title="title" v-if="messageType === '2'"
               :canCreateMessage="userStore.user.canCreateMessage1" icon="laba"/>

+ 63 - 11
src/views/main/screenEntry.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="common-box" style="margin-top: 20px">
+  <div class="common-box" style="margin-top: -40px">  <!-- 调整上边距为-24px (原-20px ×1.2) -->
     <TopTitle icon="daping" title="大屏入口" />
     <div class="sys-container">
       <div
@@ -55,9 +55,6 @@ const gotoScreen = (routeName: string) => {
   if (!isFullscreen.value) {
     enter();
   }
-  // document.body.style.transform = "scale(1)";
-  // document.body.style.transformOrigin = "top left";
-  // document.body.style.zoom = "100%";
 };
 </script>
 
@@ -67,30 +64,85 @@ const gotoScreen = (routeName: string) => {
   flex-wrap: wrap;
   justify-content: center;
   align-items: center;
+  gap: 72px; /* 原48px ×1.5 (如果原未设置gap,则使用此值) */
+  row-gap: 0px; /* 垂直间距放大1.5倍 */
+  padding: 0px 0; /* 容器内边距放大1.5倍 */
 }
 
 .sys-item {
-  min-width: 150px;
-  padding: 15px;
+  min-width: 110px; /* 150px ×1.5 */
+  padding: 11px; /* 15px ×1.5 */
   display: flex;
   flex-direction: column;
   align-items: center;
+  transition: all 0.3s ease;
+  margin-bottom: 15px; /* 底部外边距放大1.5倍 */
 
   .sys-item-img {
-    width: 160px;
-    height: 90px;
-    border-radius: 5px;
+    width: 240px; /* 160px ×1.5 */
+    height: 135px; /* 90px ×1.5 (保持16:9比例) */
+    border-radius: 7.5px; /* 5px ×1.5 */
+    object-fit: cover;
+    transition: transform 0.3s ease;
   }
 
   .sys-item-title {
-    font-size: 18px;
+    font-size: 27px; /* 18px ×1.5 */
     font-weight: bold;
-    margin-bottom: 10px;
+    margin: 15px 0; /* 10px ×1.5 */
+    text-align: center;
+    white-space: nowrap;
   }
 }
 
 .sys-item:hover {
   background-color: #87cefa;
   cursor: pointer;
+  transform: scale(1.05); /* 悬停效果保持比例 */
+
+  .sys-item-img {
+    transform: scale(1.03); /* 图片单独放大效果 */
+  }
+}
+
+/* 响应式调整 */
+@media (max-width: 1200px) {
+  .sys-container {
+    gap: 48px;
+    row-gap: 24px;
+  }
+
+  .sys-item {
+    min-width: 180px;
+
+    .sys-item-img {
+      width: 192px;
+      height: 108px;
+    }
+
+    .sys-item-title {
+      font-size: 21.6px;
+    }
+  }
+}
+
+@media (max-width: 768px) {
+  .sys-container {
+    gap: 36px;
+    row-gap: 18px;
+  }
+
+  .sys-item {
+    min-width: 150px;
+
+    .sys-item-img {
+      width: 160px;
+      height: 90px;
+    }
+
+    .sys-item-title {
+      font-size: 18px;
+    }
+  }
 }
 </style>

+ 32 - 20
src/views/systems/systems.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="common-box">
+  <div class="common-box" style="margin-top: -40px">  <!-- -40 × 1.2 = -48 -->
     <TopTitle icon="xitong" title="系统入口" />
     <div class="sys-container">
       <div
@@ -12,15 +12,13 @@
           :icon-class="item.icon"
           alt=""
           class="sys-item-img"
-          size="60"
+          size="72"
         />
         <h3 class="sys-item-title">{{ item.menuName }}</h3>
-        <!--        <div class="unclickable"></div>-->
       </div>
     </div>
   </div>
 </template>
-
 <script lang="ts" setup>
 import { useUserStore } from "@/store";
 import TopTitle from "@/components/TopTitle.vue";
@@ -42,46 +40,60 @@ const openInNewTab = (url: string) => {
   }
 };
 </script>
-
 <style lang="scss" scoped>
 .sys-container {
   display: flex;
   flex-wrap: wrap;
   justify-content: center;
   align-items: center;
+  gap: 24px; /* 新增:项目间距 (原未设置,建议添加) */
 }
 
 .sys-item {
-  min-width: 150px;
-  padding: 15px;
+  min-width: 180px; /* 150 × 1.2 = 180 */
+  padding: 18px; /* 15 × 1.2 = 18 */
   display: flex;
   flex-direction: column;
   align-items: center;
   position: relative;
+  transition: all 0.3s ease; /* 新增过渡效果 */
 
   .sys-item-img {
-    width: 60px;
+    width: 72px; /* 60 × 1.2 = 72 */
+    height: 72px; /* 保持正方形 */
   }
 
   .sys-item-title {
-    font-size: 18px;
+    font-size: 21.6px; /* 18 × 1.2 = 21.6 */
     font-weight: bold;
-    margin-bottom: 10px;
-  }
-
-  .unclickable {
-    position: absolute;
-    top: 0;
-    left: 0;
-    width: 100%;
-    height: 100%;
-    background-color: rgba(128, 128, 128, 0.5); /* 灰色蒙层 */
-    pointer-events: none; /* 使蒙层不影响点击事件 */
+    margin: 12px 0; /* 10 × 1.2 = 12 */
   }
 }
 
 .sys-item:hover {
   background-color: #87cefa;
   cursor: pointer;
+  transform: scale(1.05); /* 新增悬停放大效果 */
+}
+
+/* 响应式调整 */
+@media (max-width: 768px) {
+  .sys-container {
+    gap: 18px;
+  }
+
+  .sys-item {
+    min-width: 150px;
+    padding: 15px;
+
+    .sys-item-img {
+      width: 60px;
+      height: 60px;
+    }
+
+    .sys-item-title {
+      font-size: 18px;
+    }
+  }
 }
 </style>