批量更新,增加用户管理
This commit is contained in:
@@ -1,17 +1,28 @@
|
||||
<template>
|
||||
<el-aside :width="width" class="common-aside">
|
||||
<el-menu
|
||||
<div v-if="loading" class="loading-spinner"> <!-- Show spinner if loading -->
|
||||
<i class="el-icon-loading" style="font-size: 24px; color: #fff;"></i>
|
||||
</div>
|
||||
<el-menu v-else
|
||||
:collapse="isCollapse"
|
||||
:collapse-transition="false"
|
||||
:background-color="asideBgColor"
|
||||
:text-color="asideTextColor"
|
||||
active-text-color="#73ffed"
|
||||
:active-text-color="activeColor"
|
||||
active-background-color="activeBgColor"
|
||||
class="el-menu-vertical-demo"
|
||||
@select="handleMenuSelect"
|
||||
:default-active="route.path"
|
||||
>
|
||||
<h3 v-if="!isCollapse">管理后台</h3>
|
||||
<h3 v-else>管理</h3>
|
||||
<!-- 固定的仪表盘菜单项 -->
|
||||
<el-menu-item index="/dashboard">
|
||||
<i :class="['icons', 'fa', 'fa-solid', 'fa-gauge']"></i>
|
||||
<template #title>
|
||||
<span>仪表盘</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<template v-for="item in sortedMenuList" :key="item.path">
|
||||
<el-menu-item
|
||||
v-if="!item.children || item.children.length === 0"
|
||||
@@ -47,7 +58,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, defineEmits } from 'vue';
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useAllDataStore } from '@/stores';
|
||||
import { getAllMenus } from '@/api/menu';
|
||||
@@ -57,13 +68,15 @@ const emit = defineEmits(['menu-click']);
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const list = ref([]);
|
||||
const loading = ref(false);
|
||||
const loading = ref(true);
|
||||
|
||||
const store = useAllDataStore();
|
||||
const isCollapse = computed(() => store.state.isCollapse);
|
||||
const width = computed(() => store.state.isCollapse ? '64px' : '180px');
|
||||
const asideBgColor = ref('#0081ff');
|
||||
const asideBgColor = ref('#0074e9');
|
||||
const asideTextColor = ref('#ffffff');
|
||||
const activeColor = ref ('#fff');
|
||||
const activeBgColor = ref ('#0074e9');
|
||||
|
||||
// 更新主题颜色
|
||||
const updateThemeColors = () => {
|
||||
@@ -154,15 +167,13 @@ const transformMenuData = (menus) => {
|
||||
|
||||
// 先排序根菜单
|
||||
sortMenus(rootMenus);
|
||||
|
||||
// console.log('排序后的根菜单:', rootMenus.map(m => ({ name: m.label, order: m.order })));
|
||||
|
||||
|
||||
return rootMenus;
|
||||
};
|
||||
|
||||
// 获取菜单数据
|
||||
const fetchMenus = async () => {
|
||||
loading.value = true;
|
||||
loading.value = true;
|
||||
try {
|
||||
// 优先从 localStorage 读取
|
||||
const cachedMenus = localStorage.getItem('menuData');
|
||||
@@ -171,7 +182,6 @@ const fetchMenus = async () => {
|
||||
if (cachedMenus) {
|
||||
try {
|
||||
menuData = JSON.parse(cachedMenus);
|
||||
// console.log('从缓存读取菜单数据');
|
||||
} catch (e) {
|
||||
console.warn('缓存菜单数据解析失败:', e);
|
||||
}
|
||||
@@ -194,14 +204,12 @@ const fetchMenus = async () => {
|
||||
|
||||
// 转换并排序菜单数据
|
||||
const transformedMenus = transformMenuData(menuData);
|
||||
// console.log('转换后的菜单数据:', transformedMenus);
|
||||
// console.log('菜单顺序(按 order 排序):', transformedMenus.map(m => ({ name: m.label, order: m.order, id: m.id })));
|
||||
list.value = transformedMenus;
|
||||
} catch (error) {
|
||||
console.error('获取菜单异常:', error);
|
||||
list.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -255,8 +263,23 @@ const sortedMenuList = computed(() => {
|
||||
return sorted;
|
||||
});
|
||||
|
||||
// 固定的仪表盘菜单项
|
||||
const dashboardMenuItem = {
|
||||
path: '/dashboard',
|
||||
label: '仪表盘',
|
||||
icon: 'fa-solid fa-gauge',
|
||||
route: '/dashboard',
|
||||
order: 0
|
||||
};
|
||||
|
||||
// 菜单点击事件:emit 通知父组件
|
||||
const handleMenuSelect = (index) => {
|
||||
// 如果是仪表盘路径,直接使用固定的仪表盘菜单项
|
||||
if (index === '/dashboard') {
|
||||
emit('menu-click', dashboardMenuItem);
|
||||
return;
|
||||
}
|
||||
|
||||
const menuItem = findMenuItemByPath(list.value, index);
|
||||
if (menuItem && menuItem.route) {
|
||||
emit('menu-click', menuItem);
|
||||
@@ -281,7 +304,7 @@ const findMenuItemByPath = (menus, path) => {
|
||||
<style scoped lang="less">
|
||||
.common-aside {
|
||||
height: 100%;
|
||||
background-color: var(--aside-bg-color, #0081ff);
|
||||
background-color: var(--aside-bg-color, #0074e9);
|
||||
transition: width 0.3s, background-color 0.3s ease;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
@@ -298,48 +321,15 @@ const findMenuItemByPath = (menus, path) => {
|
||||
&:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
&.is-active {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
border-right: none;
|
||||
transition: width 0.3s;
|
||||
|
||||
h3 {
|
||||
padding: 11px 0;
|
||||
line-height: 36px;
|
||||
color: var(--aside-text-color, #fff);
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-vertical-demo {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
// 折叠状态下的样式
|
||||
.el-menu.el-menu--collapse {
|
||||
width: 64px;
|
||||
|
||||
.el-menu-item, .el-sub-menu {
|
||||
span {
|
||||
height: 0;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 非折叠状态
|
||||
.el-menu:not(.el-menu--collapse) {
|
||||
width: 180px;
|
||||
h3{
|
||||
line-height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,6 +6,13 @@
|
||||
</el-button>
|
||||
<el-breadcrumb separator="/" class="bread">
|
||||
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
|
||||
<el-breadcrumb-item
|
||||
v-for="(item, index) in breadcrumbs"
|
||||
:key="index"
|
||||
:to="(item.label === '首页' || index === breadcrumbs.length - 1) ? { path: item.path } : null"
|
||||
>
|
||||
{{ item.label }}
|
||||
</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
<div class="r-content">
|
||||
@@ -38,43 +45,72 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { useAllDataStore } from "@/stores";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { User, SwitchButton, Sunny, Moon } from '@element-plus/icons-vue';
|
||||
import { getTheme, toggleTheme as toggleThemeUtil, initTheme } from "@/utils/theme";
|
||||
import { getAllMenus } from '@/api/menu';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
interface Menu {
|
||||
id: number;
|
||||
name: string;
|
||||
path: string;
|
||||
parentId: number;
|
||||
}
|
||||
|
||||
interface Breadcrumb {
|
||||
label: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
const menuList = ref<Menu[]>([]);
|
||||
|
||||
async function loadMenu() {
|
||||
try {
|
||||
const res = await getAllMenus();
|
||||
menuList.value = res.data || [];
|
||||
} catch (error) {
|
||||
console.error('Failed to load menu', error);
|
||||
menuList.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadMenu);
|
||||
|
||||
// 根据菜单列表和当前路径计算出的面包屑导航
|
||||
const breadcrumbs = computed(() => {
|
||||
let chain: Breadcrumb[] = [];
|
||||
let currentPath = route.path || '/';
|
||||
if (currentPath === '/' || currentPath === '') {
|
||||
return [{ label: '仪表盘', path: '/' }];
|
||||
}
|
||||
let current = menuList.value.find(m => m.path === currentPath);
|
||||
if (!current) {
|
||||
const candidates = menuList.value.filter(m => currentPath.startsWith(m.path));
|
||||
current = candidates.sort((a, b) => b.path.length - a.path.length)[0];
|
||||
}
|
||||
if (!current) return [];
|
||||
chain.push({ label: current.name || 'Unknown', path: current.path });
|
||||
let parentId = current.parentId;
|
||||
while (parentId > 0) {
|
||||
let parent = menuList.value.find(m => m.id === parentId);
|
||||
if (parent && !chain.some(c => c.label === parent.name)) {
|
||||
chain.push({ label: parent.name || 'Unknown', path: parent.path });
|
||||
parentId = parent.parentId;
|
||||
} else break;
|
||||
}
|
||||
chain = chain.reverse();
|
||||
return chain;
|
||||
});
|
||||
|
||||
const store = useAllDataStore();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
// 主题相关
|
||||
const currentTheme = ref(getTheme());
|
||||
|
||||
// 主题图标
|
||||
const themeIcon = computed(() => {
|
||||
return currentTheme.value === 'dark' ? Sunny : Moon;
|
||||
});
|
||||
|
||||
// 切换主题
|
||||
const toggleTheme = () => {
|
||||
const newTheme = toggleThemeUtil();
|
||||
currentTheme.value = newTheme;
|
||||
};
|
||||
|
||||
// 监听主题变化(支持多组件同步)
|
||||
onMounted(() => {
|
||||
initTheme();
|
||||
currentTheme.value = getTheme();
|
||||
|
||||
// 监听其他组件的主题变化
|
||||
window.addEventListener('theme-change', (event) => {
|
||||
currentTheme.value = event.detail.theme;
|
||||
});
|
||||
});
|
||||
|
||||
const getImageUrl = (user) => {
|
||||
return new URL(`/src/assets/images/default_avatar.png`, import.meta.url).href;
|
||||
};
|
||||
@@ -94,6 +130,11 @@ const handleCommand = (command) => {
|
||||
//清除租户数据
|
||||
localStorage.removeItem('tenant');
|
||||
sessionStorage.removeItem('tenant');
|
||||
//清除菜单数据
|
||||
localStorage.removeItem('menus');
|
||||
sessionStorage.removeItem('menus');
|
||||
localStorage.removeItem('menuData');
|
||||
sessionStorage.removeItem('menuData');
|
||||
router.push('/login');
|
||||
}
|
||||
};
|
||||
@@ -107,7 +148,7 @@ const handleCommand = (command) => {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 40px;
|
||||
background-color: var(--header-bg-color, #0081ff);
|
||||
background-color: var(--header-bg-color, #0074e9);
|
||||
color: var(--header-text-color, #fff);
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
@@ -141,20 +182,6 @@ const handleCommand = (command) => {
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.theme-toggle-btn {
|
||||
margin-right: 0;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: var(--header-text-color, #fff);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.user {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
Reference in New Issue
Block a user