增加新布局

This commit is contained in:
2025-10-29 17:32:34 +08:00
parent 74df0e539c
commit b9938b4c0d
2866 changed files with 11551 additions and 115 deletions
+364
View File
@@ -0,0 +1,364 @@
<template>
<el-aside :width="width" class="common-aside">
<el-menu
:collapse="isCollapse"
:collapse-transition="false"
:background-color="asideBgColor"
:text-color="asideTextColor"
active-text-color="#73ffed"
class="el-menu-vertical-demo"
@select="handleMenuSelect"
:default-active="route.path"
>
<h3 v-if="!isCollapse">管理后台</h3>
<h3 v-else>管理</h3>
<template v-for="item in sortedMenuList" :key="item.path">
<el-menu-item
v-if="!item.children || item.children.length === 0"
:index="item.path"
>
<i :class="['icons', 'fa', item.icon]"></i>
<template #title>
<span>{{ item.label }}</span>
</template>
</el-menu-item>
<el-sub-menu
v-else
:index="item.path"
>
<template #title>
<i :class="['icons', 'fa', item.icon]"></i>
<span>{{ item.label }}</span>
</template>
<el-menu-item
v-for="subItem in item.children"
:key="subItem.path"
:index="subItem.path"
>
<i :class="['icons', 'fa', subItem.icon && typeof subItem.icon === 'string' ? subItem.icon.trim() : '']"></i>
<template #title>
<span>{{ subItem.label }}</span>
</template>
</el-menu-item>
</el-sub-menu>
</template>
</el-menu>
</el-aside>
</template>
<script>
import { ref, computed, onMounted } from "vue";
import { useRouter, useRoute } from "vue-router";
import { useAllDataStore } from "@/stores";
import { getAllMenus } from "@/api/menu";
export default {
name: "CommonAside",
setup() {
const router = useRouter();
const route = useRoute();
const list = ref([]);
const loading = ref(false);
const store = useAllDataStore();
const isCollapse = computed(() => store.state.isCollapse);
const width = computed(() => store.state.isCollapse ? '64px' : '180px');
// 主题颜色(用于 Element Plus 组件,需要响应式)
// 初始值设为可见的默认值,避免初始化时不可见
const asideBgColor = ref('#0081ff');
const asideTextColor = ref('#ffffff');
// 更新主题颜色
const updateThemeColors = () => {
try {
const root = document.documentElement;
const bgColor = getComputedStyle(root).getPropertyValue('--aside-bg-color').trim();
const textColor = getComputedStyle(root).getPropertyValue('--aside-text-color').trim();
// 只有当获取到有效值时才更新
if (bgColor) {
asideBgColor.value = bgColor;
}
if (textColor) {
asideTextColor.value = textColor;
}
} catch (e) {
console.warn('更新主题颜色失败:', e);
// 出错时保持默认值
}
};
// 将后端数据转换为前端需要的格式
const transformMenuData = (menus) => {
// 首先映射字段格式
const mappedMenus = menus.map(menu => ({
id: menu.id,
path: menu.path,
icon: menu.icon || 'fa-circle',
label: menu.name,
route: menu.path,
parentId: menu.parentId || 0,
order: menu.order || 0,
children: []
}));
// 构建树形结构
const menuMap = new Map();
const rootMenus = [];
// 先创建所有菜单的映射
mappedMenus.forEach(menu => {
menuMap.set(menu.id, menu);
});
// 构建树形结构
mappedMenus.forEach(menu => {
if (menu.parentId === 0) {
rootMenus.push(menu);
} else {
const parent = menuMap.get(menu.parentId);
if (parent) {
if (!parent.children) {
parent.children = [];
}
parent.children.push(menu);
} else {
// 如果找不到父节点,作为根节点处理
rootMenus.push(menu);
}
}
});
// 按 order 排序(确保排序正确)
const sortMenus = (menus) => {
if (!menus || menus.length === 0) return;
// 对当前层级的菜单进行排序
menus.sort((a, b) => {
const orderA = Number(a.order) ?? 999999; // 没有order的排在最后
const orderB = Number(b.order) ?? 999999;
const diff = orderA - orderB;
// 如果 order 相同,按 id 排序(保证稳定性)
if (diff === 0) {
return (a.id || 0) - (b.id || 0);
}
return diff;
});
// 递归排序子菜单
menus.forEach(menu => {
if (menu.children && menu.children.length > 0) {
sortMenus(menu.children);
}
});
};
// 先排序根菜单
sortMenus(rootMenus);
// console.log('排序后的根菜单:', rootMenus.map(m => ({ name: m.label, order: m.order })));
return rootMenus;
};
// 获取菜单数据
const fetchMenus = async () => {
loading.value = true;
try {
// 优先从 localStorage 读取
const cachedMenus = localStorage.getItem('menuData');
let menuData = null;
if (cachedMenus) {
try {
menuData = JSON.parse(cachedMenus);
// console.log('从缓存读取菜单数据');
} catch (e) {
console.warn('缓存菜单数据解析失败:', e);
}
}
// 如果缓存中没有或解析失败,从接口获取
if (!menuData) {
const res = await getAllMenus();
if (res && res.success && res.data) {
menuData = res.data;
// 保存到缓存
localStorage.setItem('menuData', JSON.stringify(menuData));
} else {
console.error('获取菜单失败:', res?.message || '未知错误');
list.value = [];
loading.value = false;
return;
}
}
// 转换并排序菜单数据
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;
}
};
// 组件挂载时获取菜单
onMounted(() => {
// 先更新主题颜色,确保组件可见
updateThemeColors();
// 延迟一点获取菜单,确保主题已初始化
setTimeout(() => {
fetchMenus();
}, 100);
// 监听主题变化事件
window.addEventListener('theme-change', updateThemeColors);
// 监听 CSS 变量变化(MutationObserver
const observer = new MutationObserver(updateThemeColors);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-theme']
});
});
// 计算属性:统一排序所有菜单项(不再区分有无子菜单)
const sortedMenuList = computed(() => {
// 创建副本并排序,确保按 order 排序
const sorted = [...list.value].sort((a, b) => {
const orderA = Number(a.order) ?? 999999;
const orderB = Number(b.order) ?? 999999;
if (orderA === orderB) {
return (a.id || 0) - (b.id || 0);
}
return orderA - orderB;
});
// 确保子菜单也排序
sorted.forEach(menu => {
if (menu.children && menu.children.length > 0) {
menu.children.sort((a, b) => {
const orderA = Number(a.order) ?? 999999;
const orderB = Number(b.order) ?? 999999;
if (orderA === orderB) {
return (a.id || 0) - (b.id || 0);
}
return orderA - orderB;
});
}
});
return sorted;
});
// 菜单点击事件处理
const handleMenuSelect = (index) => {
const menuItem = findMenuItemByPath(list.value, index);
if (menuItem && menuItem.route) {
router.push(menuItem.route);
}
};
// 递归查找菜单项
const findMenuItemByPath = (menus, path) => {
for (const menu of menus) {
if (menu.path === path) {
return menu;
}
if (menu.children && menu.children.length > 0) {
const found = findMenuItemByPath(menu.children, path);
if (found) return found;
}
}
return null;
};
return {
list,
sortedMenuList,
store,
isCollapse,
width,
loading,
handleMenuSelect,
route,
asideBgColor,
asideTextColor
};
}
};
</script>
<style scoped lang="less">
.common-aside {
height: 100%;
background-color: var(--aside-bg-color, #0081ff);
transition: width 0.3s, background-color 0.3s ease;
overflow: hidden;
position: relative;
display: block !important;
visibility: visible !important;
}
.icons {
width: 16px;
height: 16px;
margin-right: 8px;
font-size: 16px;
transition: var(--transition-fast);
&: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;
}
</style>
+220
View File
@@ -0,0 +1,220 @@
<template>
<div class="header">
<div class="l-content">
<el-button size="small" @click="handleCollapse">
<i class="fa fa-bars"></i>
</el-button>
<el-breadcrumb separator="/" class="bread">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="r-content">
<!-- 主题切换按钮 -->
<el-button
circle
:icon="themeIcon"
@click="toggleTheme"
class="theme-toggle-btn"
:title="currentTheme === 'dark' ? '切换到亮色模式' : '切换到暗色模式'"
/>
<el-dropdown trigger="click" @command="handleCommand">
<span class="el-dropdown-link" style="cursor: pointer;">
<img :src="getImageUrl('user')" class="user" />
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile">
<el-icon><User /></el-icon>
<span>个人中心</span>
</el-dropdown-item>
<el-dropdown-item divided command="logout">
<el-icon><SwitchButton /></el-icon>
<span>退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from "vue";
import { useRouter } 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";
const router = useRouter();
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;
};
const handleCollapse = () => {
store.state.isCollapse = !store.state.isCollapse;
};
const handleCommand = (command) => {
if (command === 'profile') {
router.push('/user/userProfile');
} else if (command === 'logout') {
authStore.clearToken();
// 清除缓存中的user数据
localStorage.removeItem('user');
sessionStorage.removeItem('user');
router.push('/login');
}
};
</script>
<style scoped lang="less">
.header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
height: 100%;
padding: 0 40px;
background-color: var(--header-bg-color, #0081ff);
color: var(--header-text-color, #fff);
transition: background-color 0.3s ease, color 0.3s ease;
}
.icons {
width: 20px;
height: 20px;
}
.l-content {
display: flex;
align-items: center;
.el-button {
margin-right: 20px;
background-color: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
color: var(--header-text-color, #fff);
&:hover {
background-color: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.3);
}
}
}
.r-content {
position: relative;
z-index: 1000;
display: flex;
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;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.3);
transition: border-color 0.3s ease;
}
.el-dropdown-link {
display: flex;
align-items: center;
cursor: pointer;
}
}
// 下拉菜单样式 - 使用全局样式覆盖
:deep(.el-dropdown) {
.el-dropdown__popper {
.el-dropdown-menu {
background-color: var(--bg-color-overlay) !important;
border-color: var(--border-color) !important;
padding: 4px 0;
.el-dropdown-menu__item {
display: flex;
align-items: center;
gap: 8px;
color: var(--text-color-primary) !important;
transition: background-color 0.2s ease, color 0.2s ease;
span {
margin-left: 0;
}
.el-icon {
color: var(--text-color-primary) !important;
}
&:not(.is-disabled):hover {
background-color: var(--fill-color-light) !important;
color: var(--text-color-primary) !important;
}
&.is-divided {
border-top-color: var(--border-color) !important;
}
}
}
}
}
:deep(.bread) {
.el-breadcrumb__inner {
color: var(--header-text-color, #fff) !important;
}
.el-breadcrumb__inner.is-link {
color: var(--header-text-color, #fff) !important;
cursor: pointer !important;
&:hover {
opacity: 0.8;
}
}
}
</style>