|
@@ -1,25 +1,34 @@
|
|
|
<!-- layout/index.vue -->
|
|
<!-- layout/index.vue -->
|
|
|
<template>
|
|
<template>
|
|
|
- <div
|
|
|
|
|
- class="app-wrapper"
|
|
|
|
|
- :style="{
|
|
|
|
|
- '--current-color': theme,
|
|
|
|
|
- '--current-color-light': theme + '1a',
|
|
|
|
|
- '--current-color-dark-bg': theme + '33',
|
|
|
|
|
- }"
|
|
|
|
|
- >
|
|
|
|
|
- <!-- 顶部栏(系统名 + 用户信息) -->
|
|
|
|
|
|
|
+ <div class="app-wrapper" :style="{
|
|
|
|
|
+ '--current-color': theme,
|
|
|
|
|
+ '--current-color-light': theme + '1a',
|
|
|
|
|
+ '--current-color-dark-bg': theme + '33',
|
|
|
|
|
+ }">
|
|
|
|
|
+ <!-- 顶部栏(系统名 + 一级菜单 + 用户信息) -->
|
|
|
<div class="top-bar">
|
|
<div class="top-bar">
|
|
|
<div class="logo-area">
|
|
<div class="logo-area">
|
|
|
<img :src="logoImg" class="logo-img" />
|
|
<img :src="logoImg" class="logo-img" />
|
|
|
- <span class="logo-text" style="cursor:pointer" @click="router.push('/index')">水利安全生产监管信息系统</span>
|
|
|
|
|
|
|
+ <span class="logo-text" style="cursor:pointer" @click="router.push('/')">水利安全生产监管信息系统</span>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 一级菜单(顶部水平导航) -->
|
|
|
|
|
+ <div class="top-nav-menu">
|
|
|
|
|
+ <div v-for="item in topMenus" :key="item.path"
|
|
|
|
|
+ :class="['top-nav-item', { active: activeTopMenu === item.path }]" @click="handleTopMenuClick(item)">
|
|
|
|
|
+ <svg-icon v-if="item.meta?.icon" :icon-class="item.meta.icon" class="top-nav-icon" />
|
|
|
|
|
+ <span>{{ item.meta?.title }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
<div class="top-right-menu">
|
|
<div class="top-right-menu">
|
|
|
<el-dropdown @command="handleCommand" class="avatar-container" trigger="hover">
|
|
<el-dropdown @command="handleCommand" class="avatar-container" trigger="hover">
|
|
|
<div class="avatar-wrapper">
|
|
<div class="avatar-wrapper">
|
|
|
<svg-icon icon-class="user" class="user-icon" />
|
|
<svg-icon icon-class="user" class="user-icon" />
|
|
|
<span class="user-realName">{{ userStore.realName || userStore.name }}</span>
|
|
<span class="user-realName">{{ userStore.realName || userStore.name }}</span>
|
|
|
- <el-icon class="dropdown-icon"><ArrowDown /></el-icon>
|
|
|
|
|
|
|
+ <el-icon class="dropdown-icon">
|
|
|
|
|
+ <ArrowDown />
|
|
|
|
|
+ </el-icon>
|
|
|
</div>
|
|
</div>
|
|
|
<template #dropdown>
|
|
<template #dropdown>
|
|
|
<el-dropdown-menu>
|
|
<el-dropdown-menu>
|
|
@@ -35,13 +44,13 @@
|
|
|
|
|
|
|
|
<!-- 下方区域:左右结构 -->
|
|
<!-- 下方区域:左右结构 -->
|
|
|
<div class="main-layout">
|
|
<div class="main-layout">
|
|
|
- <!-- 左侧二三级菜单 -->
|
|
|
|
|
- <div class="sidebar-wrapper" :class="{ collapsed: isCollapse }">
|
|
|
|
|
|
|
+ <!-- 左侧菜单:有子菜单时才显示 -->
|
|
|
|
|
+ <div class="sidebar-wrapper" :class="{ collapsed: isCollapse }" v-show="!sidebar.hide">
|
|
|
<sidebar v-if="!sidebar.hide" />
|
|
<sidebar v-if="!sidebar.hide" />
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- 右侧内容区域 -->
|
|
<!-- 右侧内容区域 -->
|
|
|
- <div class="content-wrapper">
|
|
|
|
|
|
|
+ <div class="content-wrapper" :class="{ 'full-width': sidebar.hide }">
|
|
|
<app-main />
|
|
<app-main />
|
|
|
<settings ref="settingRef" />
|
|
<settings ref="settingRef" />
|
|
|
</div>
|
|
</div>
|
|
@@ -50,8 +59,8 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup>
|
|
<script setup>
|
|
|
-import { computed, ref } from "vue";
|
|
|
|
|
-import { useRouter } from "vue-router";
|
|
|
|
|
|
|
+import { computed, ref, watch } from "vue";
|
|
|
|
|
+import { useRouter, useRoute } from "vue-router";
|
|
|
import { ElMessageBox } from "element-plus";
|
|
import { ElMessageBox } from "element-plus";
|
|
|
import { ArrowDown } from "@element-plus/icons-vue";
|
|
import { ArrowDown } from "@element-plus/icons-vue";
|
|
|
import Sidebar from "./components/Sidebar/index.vue";
|
|
import Sidebar from "./components/Sidebar/index.vue";
|
|
@@ -59,12 +68,15 @@ import { AppMain, Settings } from "./components";
|
|
|
import useAppStore from "@/store/modules/app";
|
|
import useAppStore from "@/store/modules/app";
|
|
|
import useSettingsStore from "@/store/modules/settings";
|
|
import useSettingsStore from "@/store/modules/settings";
|
|
|
import useUserStore from "@/store/modules/user";
|
|
import useUserStore from "@/store/modules/user";
|
|
|
|
|
+import usePermissionStore from "@/store/modules/permission";
|
|
|
import logoImg from "@/assets/logo/logo.png";
|
|
import logoImg from "@/assets/logo/logo.png";
|
|
|
|
|
|
|
|
const settingsStore = useSettingsStore();
|
|
const settingsStore = useSettingsStore();
|
|
|
const appStore = useAppStore();
|
|
const appStore = useAppStore();
|
|
|
const userStore = useUserStore();
|
|
const userStore = useUserStore();
|
|
|
|
|
+const permissionStore = usePermissionStore();
|
|
|
const router = useRouter();
|
|
const router = useRouter();
|
|
|
|
|
+const route = useRoute();
|
|
|
|
|
|
|
|
const theme = computed(() => settingsStore.theme);
|
|
const theme = computed(() => settingsStore.theme);
|
|
|
const sidebar = computed(() => appStore.sidebar);
|
|
const sidebar = computed(() => appStore.sidebar);
|
|
@@ -72,6 +84,162 @@ const isCollapse = computed(() => appStore.sidebar.isCollapse);
|
|
|
|
|
|
|
|
const settingRef = ref(null);
|
|
const settingRef = ref(null);
|
|
|
|
|
|
|
|
|
|
+// ========== 顶部一级菜单 ==========
|
|
|
|
|
+
|
|
|
|
|
+// 从 topbarRouters 提取一级菜单
|
|
|
|
|
+const topMenus = computed(() => {
|
|
|
|
|
+ const routes = permissionStore.topbarRouters || [];
|
|
|
|
|
+ const menus = [];
|
|
|
|
|
+ for (const menu of routes) {
|
|
|
|
|
+ if (menu.hidden) continue;
|
|
|
|
|
+ // 根路径:取其第一个子路由(首页)
|
|
|
|
|
+ if ((menu.path === "/" || menu.path === "") && menu.children && menu.children.length > 0) {
|
|
|
|
|
+ const firstChild = menu.children.find(c => !c.hidden) || menu.children[0];
|
|
|
|
|
+ menus.push({
|
|
|
|
|
+ ...firstChild,
|
|
|
|
|
+ path: firstChild.path.startsWith("/") ? firstChild.path : "/" + firstChild.path,
|
|
|
|
|
+ });
|
|
|
|
|
+ } else if (menu.meta?.title) {
|
|
|
|
|
+ menus.push(menu);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return menus;
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 当前激活的一级菜单
|
|
|
|
|
+const activeTopMenu = ref("/index");
|
|
|
|
|
+
|
|
|
|
|
+// 根据当前路由路径找到归属的一级菜单
|
|
|
|
|
+function findActiveTopMenu(path) {
|
|
|
|
|
+ if (path === "/index" || path === "/") return "/index";
|
|
|
|
|
+
|
|
|
|
|
+ // 优先检查 meta.activeMenu(隐藏路由通过此属性指定归属菜单)
|
|
|
|
|
+ if (route.meta?.activeMenu) {
|
|
|
|
|
+ const activePath = route.meta.activeMenu;
|
|
|
|
|
+ const found = topMenus.value.find(m => activePath === m.path || activePath.startsWith(m.path + "/"));
|
|
|
|
|
+ if (found) return found.path;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 查找 path 属于哪个一级菜单
|
|
|
|
|
+ for (const menu of topMenus.value) {
|
|
|
|
|
+ if (path === menu.path || path.startsWith(menu.path + "/")) {
|
|
|
|
|
+ return menu.path;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return "/index";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 同步左侧菜单:根据当前激活的一级菜单,设置侧边栏内容
|
|
|
|
|
+function syncSidebar(topMenuPath) {
|
|
|
|
|
+ // 首页永远不显示侧边栏
|
|
|
|
|
+ if (topMenuPath === "/index") {
|
|
|
|
|
+ permissionStore.setSidebarRouters([]);
|
|
|
|
|
+ appStore.toggleSideBarHide(true);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const topRoutes = permissionStore.topbarRouters || [];
|
|
|
|
|
+ const fullRoute = topRoutes.find(r => r.path === topMenuPath);
|
|
|
|
|
+
|
|
|
|
|
+ if (fullRoute && fullRoute.children && fullRoute.children.length > 0) {
|
|
|
|
|
+ const visibleChildren = fullRoute.children.filter(c => !c.hidden && c.meta?.title);
|
|
|
|
|
+ if (visibleChildren.length > 0) {
|
|
|
|
|
+ // 构建侧边栏路由:子路径拼成绝对路径
|
|
|
|
|
+ const basePath = (!fullRoute.path || fullRoute.path === "/") ? "" : fullRoute.path;
|
|
|
|
|
+ const sidebarRoutes = visibleChildren.map(c => ({
|
|
|
|
|
+ ...c,
|
|
|
|
|
+ path: c.path.startsWith("/") ? c.path : (basePath + "/" + c.path),
|
|
|
|
|
+ }));
|
|
|
|
|
+ permissionStore.setSidebarRouters(sidebarRoutes);
|
|
|
|
|
+ appStore.toggleSideBarHide(false);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 没有子菜单:隐藏侧边栏
|
|
|
|
|
+ permissionStore.setSidebarRouters([]);
|
|
|
|
|
+ appStore.toggleSideBarHide(true);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 点击顶部一级菜单
|
|
|
|
|
+function handleTopMenuClick(menu) {
|
|
|
|
|
+ const menuPath = menu.path;
|
|
|
|
|
+
|
|
|
|
|
+ // 首页直接导航,不显示侧边栏
|
|
|
|
|
+ if (menuPath === "/index") {
|
|
|
|
|
+ appStore.toggleSideBarHide(true);
|
|
|
|
|
+ permissionStore.setSidebarRouters([]);
|
|
|
|
|
+ router.push("/index");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 查找该菜单在 topbarRouters 中的完整路由
|
|
|
|
|
+ const topRoutes = permissionStore.topbarRouters || [];
|
|
|
|
|
+ const fullRoute = topRoutes.find(r => r.path === menuPath);
|
|
|
|
|
+
|
|
|
|
|
+ if (fullRoute && fullRoute.children && fullRoute.children.length > 0) {
|
|
|
|
|
+ const visibleChildren = fullRoute.children.filter(c => !c.hidden && c.meta?.title);
|
|
|
|
|
+ if (visibleChildren.length > 0) {
|
|
|
|
|
+ // 有子菜单:更新侧边栏,导航到第一个可见子菜单
|
|
|
|
|
+ const basePath = (!fullRoute.path || fullRoute.path === "/") ? "" : fullRoute.path;
|
|
|
|
|
+ const sidebarRoutes = visibleChildren.map(c => ({
|
|
|
|
|
+ ...c,
|
|
|
|
|
+ path: c.path.startsWith("/") ? c.path : (basePath + "/" + c.path),
|
|
|
|
|
+ }));
|
|
|
|
|
+ permissionStore.setSidebarRouters(sidebarRoutes);
|
|
|
|
|
+ appStore.toggleSideBarHide(false);
|
|
|
|
|
+
|
|
|
|
|
+ const firstChild = sidebarRoutes[0];
|
|
|
|
|
+ let navigatePath;
|
|
|
|
|
+ if (firstChild.children && firstChild.children.length > 0) {
|
|
|
|
|
+ const firstGrandchild = firstChild.children.find(cc => !cc.hidden) || firstChild.children[0];
|
|
|
|
|
+ navigatePath = firstGrandchild.path.startsWith("/")
|
|
|
|
|
+ ? firstGrandchild.path
|
|
|
|
|
+ : firstChild.path + "/" + firstGrandchild.path;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ navigatePath = firstChild.path;
|
|
|
|
|
+ }
|
|
|
|
|
+ router.push(navigatePath);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 没有子菜单直接导航,隐藏侧边栏
|
|
|
|
|
+ appStore.toggleSideBarHide(true);
|
|
|
|
|
+ permissionStore.setSidebarRouters([]);
|
|
|
|
|
+ router.push(menuPath);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 初始化:setup 阶段同步执行,确保首帧渲染前侧边栏已正确
|
|
|
|
|
+if (permissionStore.topbarRouters && permissionStore.topbarRouters.length > 0) {
|
|
|
|
|
+ const initTop = findActiveTopMenu(route.path);
|
|
|
|
|
+ activeTopMenu.value = initTop;
|
|
|
|
|
+ syncSidebar(initTop);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 菜单数据就绪时(首次加载异步返回后)补充同步一次
|
|
|
|
|
+watch(topMenus, (menus) => {
|
|
|
|
|
+ if (menus.length === 0) return;
|
|
|
|
|
+ const top = findActiveTopMenu(route.path);
|
|
|
|
|
+ if (top !== activeTopMenu.value) {
|
|
|
|
|
+ activeTopMenu.value = top;
|
|
|
|
|
+ syncSidebar(top);
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 路由变化时同步侧边栏
|
|
|
|
|
+watch(
|
|
|
|
|
+ () => route.path,
|
|
|
|
|
+ (newPath) => {
|
|
|
|
|
+ if (topMenus.value.length === 0) return;
|
|
|
|
|
+ const top = findActiveTopMenu(newPath);
|
|
|
|
|
+ if (top !== activeTopMenu.value) {
|
|
|
|
|
+ activeTopMenu.value = top;
|
|
|
|
|
+ syncSidebar(top);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
function handleCommand(command) {
|
|
function handleCommand(command) {
|
|
|
switch (command) {
|
|
switch (command) {
|
|
|
case "logout":
|
|
case "logout":
|
|
@@ -83,7 +251,7 @@ function handleCommand(command) {
|
|
|
userStore.logOut().then(() => {
|
|
userStore.logOut().then(() => {
|
|
|
location.href = "/index";
|
|
location.href = "/index";
|
|
|
});
|
|
});
|
|
|
- }).catch(() => {});
|
|
|
|
|
|
|
+ }).catch(() => { });
|
|
|
break;
|
|
break;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -96,13 +264,14 @@ function handleCommand(command) {
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/* ===== 顶部系统栏 ===== */
|
|
|
.top-bar {
|
|
.top-bar {
|
|
|
height: 60px;
|
|
height: 60px;
|
|
|
background: #3c68cb;
|
|
background: #3c68cb;
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
justify-content: space-between;
|
|
|
- padding: 0 30px;
|
|
|
|
|
|
|
+ padding: 0 20px;
|
|
|
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
|
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
|
|
flex-shrink: 0;
|
|
flex-shrink: 0;
|
|
|
}
|
|
}
|
|
@@ -110,6 +279,8 @@ function handleCommand(command) {
|
|
|
.logo-area {
|
|
.logo-area {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ margin-right: 80px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.logo-img {
|
|
.logo-img {
|
|
@@ -122,11 +293,73 @@ function handleCommand(command) {
|
|
|
color: #fff;
|
|
color: #fff;
|
|
|
font-size: 20px;
|
|
font-size: 20px;
|
|
|
font-weight: bold;
|
|
font-weight: bold;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 顶部一级菜单 */
|
|
|
|
|
+.top-nav-menu {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ overflow-x: auto;
|
|
|
|
|
+ overflow-y: hidden;
|
|
|
|
|
+ scrollbar-width: thin;
|
|
|
|
|
+ scrollbar-color: rgba(255, 255, 255, 0.25) transparent;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.top-nav-menu::-webkit-scrollbar {
|
|
|
|
|
+ height: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.top-nav-menu::-webkit-scrollbar-track {
|
|
|
|
|
+ background: transparent;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.top-nav-menu::-webkit-scrollbar-thumb {
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.25);
|
|
|
|
|
+ border-radius: 2px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.top-nav-menu::-webkit-scrollbar-thumb:hover {
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.45);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.top-nav-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ padding: 0 18px;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ color: rgba(255, 255, 255, 0.85);
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ transition: background 0.2s, color 0.2s;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.top-nav-item:hover {
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.12);
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.top-nav-item.active {
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.22);
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.top-nav-icon {
|
|
|
|
|
+ width: 16px;
|
|
|
|
|
+ height: 16px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.top-right-menu {
|
|
.top-right-menu {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ margin-left: 12px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.avatar-wrapper {
|
|
.avatar-wrapper {
|
|
@@ -170,6 +403,7 @@ function handleCommand(command) {
|
|
|
transform: rotate(180deg);
|
|
transform: rotate(180deg);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/* ===== 下方主布局 ===== */
|
|
|
.main-layout {
|
|
.main-layout {
|
|
|
flex: 1;
|
|
flex: 1;
|
|
|
display: flex;
|
|
display: flex;
|
|
@@ -189,6 +423,9 @@ function handleCommand(command) {
|
|
|
.content-wrapper {
|
|
.content-wrapper {
|
|
|
flex: 1;
|
|
flex: 1;
|
|
|
overflow: auto;
|
|
overflow: auto;
|
|
|
- //background: #f0f2f6;
|
|
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.content-wrapper.full-width {
|
|
|
|
|
+ margin-left: 0;
|
|
|
}
|
|
}
|
|
|
</style>
|
|
</style>
|