批量更新,增加用户管理

This commit is contained in:
2025-11-01 17:57:34 +08:00
parent 186dcb86d9
commit bfba8d7ad6
28 changed files with 2307 additions and 2377 deletions
+45 -55
View File
@@ -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>
+70 -43
View File
@@ -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;