1293 lines
31 KiB
Vue
1293 lines
31 KiB
Vue
<template>
|
||
<div class="home">
|
||
<!-- 上半部分:校园风景图片轮播 -->
|
||
<div class="carousel-section">
|
||
<el-carousel :interval="4000" type="card" height="200px" indicator-position="outside" arrow="always" class="campus-carousel">
|
||
<el-carousel-item v-for="(image, index) in campusImages" :key="index">
|
||
<div class="carousel-item">
|
||
<img :src="image.url" :alt="image.title" class="carousel-image" />
|
||
<div class="carousel-overlay">
|
||
<h3 class="carousel-title">{{ image.title }}</h3>
|
||
</div>
|
||
</div>
|
||
</el-carousel-item>
|
||
</el-carousel>
|
||
</div>
|
||
|
||
<!-- 菜单功能区域 -->
|
||
<div class="menu-modules-section">
|
||
<h2 class="section-title">
|
||
<div class="section-icon">
|
||
<img src="/src/assets/logo-img/category.svg" alt="系统功能" class="section-icon-img" />
|
||
</div>
|
||
系统功能
|
||
<span class="function-count" v-if="allMenuActions.length > 0"> ({{ menuModules.length }}个模块,{{ allMenuActions.length }}个功能) </span>
|
||
</h2>
|
||
|
||
<!-- 按模块分组显示菜单 -->
|
||
<div v-for="module in menuModules" :key="module.id" class="module-group">
|
||
<div class="module-header">
|
||
<h3 class="module-title" style="color: #000000">
|
||
<div class="module-icon-wrapper">
|
||
<img src="/src/assets/logo-img/zj.svg" alt="模块图标" class="module-icon-img" />
|
||
</div>
|
||
{{ module.title }}
|
||
<el-tag type="info" size="small" class="module-count-tag"> {{ module.menus.length }}个功能 </el-tag>
|
||
</h3>
|
||
<!-- <div class="module-description">{{ module.description }}</div> -->
|
||
</div>
|
||
|
||
<!-- 使用弹性布局替换el-row和el-col -->
|
||
<div class="module-content-flex">
|
||
<div v-for="menu in module.menus" :key="menu.id" class="menu-card-wrapper">
|
||
<el-card shadow="hover" :class="['menu-action-card', { 'disabled': !menu.available }]" @click="handleQuickAction(menu)">
|
||
<div class="menu-action">
|
||
<!-- 使用随机logo图片替换图标 -->
|
||
<div class="menu-logo-wrapper">
|
||
<img :src="menu.logoUrl" :alt="menu.title" class="menu-logo" />
|
||
</div>
|
||
<span class="action-title" :class="{ 'disabled': !menu.available }">{{ menu.title }}</span>
|
||
<!-- <span class="action-desc" :class="{ 'disabled': !menu.available }">{{ menu.description }}</span> -->
|
||
<!-- 移除层级路径显示 -->
|
||
<el-tag v-if="!menu.available" type="info" size="small" class="status-tag">开发中</el-tag>
|
||
</div>
|
||
</el-card>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 如果没有菜单数据,显示加载状态 -->
|
||
<div v-if="menuModules.length === 0" class="loading-state">
|
||
<el-empty description="正在加载菜单数据..." :image-size="100">
|
||
<template #description>
|
||
<p>正在从系统获取菜单权限数据...</p>
|
||
<p class="loading-tips">如果长时间未加载,请检查网络连接或刷新页面</p>
|
||
</template>
|
||
</el-empty>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup name="Index" lang="ts">
|
||
import { ref, onMounted, computed } from 'vue';
|
||
import {
|
||
User,
|
||
Tools,
|
||
Calendar,
|
||
Service,
|
||
Phone,
|
||
Message,
|
||
Document,
|
||
DataAnalysis,
|
||
Bell,
|
||
Setting,
|
||
Monitor,
|
||
Files,
|
||
Folder
|
||
} from '@element-plus/icons-vue';
|
||
import { ElMessage } from 'element-plus/es/components/message/index';
|
||
import { useRouter } from 'vue-router';
|
||
import { getCurrentInstance, ComponentInternalInstance } from 'vue';
|
||
// 导入权限store来获取菜单数据
|
||
import { usePermissionStore } from '@/store/modules/permission';
|
||
|
||
const router = useRouter();
|
||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||
const permissionStore = usePermissionStore();
|
||
|
||
// 校园风景图片数据
|
||
const campusImages = [
|
||
{
|
||
url: '/assets/images/01.jpg',
|
||
title: '西溪校园正门'
|
||
},
|
||
{
|
||
url: '/assets/images/02.jpg',
|
||
title: '临安校区'
|
||
},
|
||
{
|
||
url: '/assets/images/03.jpg',
|
||
title: '临安校区'
|
||
}
|
||
];
|
||
|
||
// 菜单模块数据
|
||
interface MenuModule {
|
||
id: string;
|
||
title: string;
|
||
description: string;
|
||
color: string;
|
||
menus: MenuAction[];
|
||
}
|
||
|
||
interface MenuAction {
|
||
id: string;
|
||
title: string;
|
||
description: string;
|
||
icon: any;
|
||
color: string;
|
||
route: string;
|
||
available: boolean;
|
||
parentName?: string;
|
||
fullPath?: string; // 保留但不再显示
|
||
menuType?: string;
|
||
visible?: string;
|
||
status?: string;
|
||
moduleOrder?: number; // 新增:用于模块排序
|
||
menuOrder?: number; // 新增:用于菜单项排序
|
||
orderNum?: number; // 新增:用于存储菜单的原始排序号
|
||
logoUrl?: string; // 新增:用于存储随机logo图片路径
|
||
isExternal?: boolean; // 是否外链
|
||
}
|
||
|
||
// 菜单模块配置
|
||
const menuModules = ref<MenuModule[]>([]);
|
||
const allMenuActions = ref<MenuAction[]>([]);
|
||
|
||
// 根据icon名称获取对应的图标路径
|
||
const getIconPath = (iconName: string): string => {
|
||
// 定义可用的图标文件列表(基于logo-img目录中的实际文件)
|
||
const availableSvgs = [
|
||
'workflow',
|
||
'tree',
|
||
'tree-table',
|
||
'tag',
|
||
'system',
|
||
'study',
|
||
'row',
|
||
'rate',
|
||
'radio',
|
||
'process-definition',
|
||
'people',
|
||
'peoples',
|
||
'pdf',
|
||
// 'other',
|
||
'Notice',
|
||
'nested',
|
||
'my-task',
|
||
'my-copy',
|
||
'money',
|
||
'logininfor',
|
||
'log',
|
||
'lock',
|
||
'list',
|
||
'international',
|
||
'eye',
|
||
'empty',
|
||
'finish',
|
||
'email',
|
||
'education',
|
||
'edit',
|
||
'druid',
|
||
'download',
|
||
'date-range',
|
||
'dashboard',
|
||
'component',
|
||
'company',
|
||
'color',
|
||
'clipboard',
|
||
'checkbox',
|
||
'chart',
|
||
'category',
|
||
'button',
|
||
'build',
|
||
'404'
|
||
];
|
||
|
||
const availablePngs = [
|
||
'table',
|
||
'shopping',
|
||
'redis-list',
|
||
'post',
|
||
'my-task',
|
||
'model',
|
||
'link',
|
||
'language',
|
||
'job',
|
||
'input',
|
||
'icon',
|
||
'guide',
|
||
'form',
|
||
'finish',
|
||
'eye-open',
|
||
'experience',
|
||
'exit-fullscreen1',
|
||
'exit-fullscreen',
|
||
'excel',
|
||
'example',
|
||
'drag',
|
||
'documentation',
|
||
'dict',
|
||
'date',
|
||
'code',
|
||
'cascader',
|
||
'caret-forward',
|
||
'caret-back',
|
||
'bug',
|
||
'other',
|
||
'password'
|
||
];
|
||
|
||
// 先尝试匹配.svg文件
|
||
if (availableSvgs.includes(iconName)) {
|
||
return `/src/assets/logo-img/${iconName}.svg`;
|
||
}
|
||
|
||
// 如果.svg不存在,尝试匹配.png文件
|
||
if (availablePngs.includes(iconName)) {
|
||
return `/src/assets/logo-img/${iconName}.png`;
|
||
}
|
||
|
||
// 如果都不存在,使用默认图标
|
||
return `/src/assets/logo-img/other.png`;
|
||
};
|
||
|
||
// 图标映射
|
||
const iconMap: { [key: string]: any } = {
|
||
'user': User,
|
||
'peoples': User,
|
||
'system': Setting,
|
||
'monitor': Monitor,
|
||
'tool': Tools,
|
||
'guide': Document,
|
||
'build': Tools,
|
||
'international': Files,
|
||
'bug': Tools,
|
||
'chart': DataAnalysis,
|
||
'dashboard': Monitor,
|
||
'form': Document,
|
||
'table': Files,
|
||
'tree': Files,
|
||
'tree-table': Files,
|
||
'example': Document,
|
||
'edit': Document,
|
||
'dict': Files,
|
||
'config': Setting,
|
||
'logininfor': Monitor,
|
||
'online': Monitor,
|
||
'druid': Monitor,
|
||
'server': Monitor,
|
||
'cache': Monitor,
|
||
'gen': Tools,
|
||
'swagger': Document,
|
||
'job': Tools,
|
||
'client': Service,
|
||
'tenant': User,
|
||
'package': Files,
|
||
'post': Service,
|
||
'role': Service,
|
||
'menu': Setting,
|
||
'dept': Files,
|
||
'notice': Bell,
|
||
'log': Monitor
|
||
};
|
||
|
||
// 模块颜色配置
|
||
const moduleColors = [
|
||
'#409EFF',
|
||
'#67C23A',
|
||
'#E6A23C',
|
||
'#F56C6C',
|
||
'#909399',
|
||
'#9966CC',
|
||
'#FF6B6B',
|
||
'#4ECDC4',
|
||
'#45B7D1',
|
||
'#96CEB4',
|
||
'#FFEAA7',
|
||
'#DDA0DD'
|
||
];
|
||
|
||
// 菜单数据扁平化处理函数 - 重写以正确识别多级目录结构
|
||
const flattenMenus = (menus: any[], parentChain: string[] = [], parentPath = '', level = 0): MenuAction[] => {
|
||
const result: MenuAction[] = [];
|
||
|
||
menus.forEach((menu, menuIndex) => {
|
||
// 跳过隐藏的菜单
|
||
if (menu.hidden === true) {
|
||
console.log(`跳过隐藏菜单: ${menu.meta?.title || menu.name}`);
|
||
return;
|
||
}
|
||
|
||
// 添加调试信息,特别关注工作流菜单
|
||
if (menu.meta?.title === '工作流' || menu.name === 'Workflow11616') {
|
||
console.log('=== 工作流菜单处理开始 ===');
|
||
console.log('工作流菜单原始数据:', menu);
|
||
console.log('工作流菜单组件类型:', menu.component);
|
||
console.log('工作流菜单是否有子菜单:', !!menu.children);
|
||
console.log('工作流菜单子菜单数量:', menu.children?.length || 0);
|
||
console.log('工作流菜单层级:', level);
|
||
console.log('工作流菜单父路径:', parentPath);
|
||
}
|
||
|
||
// 识别外链菜单
|
||
const rawLink = menu.meta?.link || menu.path;
|
||
const isExternalLink = typeof rawLink === 'string' && /^https?:\/\//i.test(rawLink);
|
||
|
||
// 构建完整路径 - 外链直接使用原始链接,内部路由按层级拼接
|
||
let fullPath = menu.path;
|
||
if (isExternalLink) {
|
||
fullPath = rawLink;
|
||
} else {
|
||
// 如果当前路径不是以 '/' 开头,需要拼接父路径
|
||
if (!menu.path.startsWith('/')) {
|
||
if (parentPath) {
|
||
// 确保父路径以 '/' 开头
|
||
const normalizedParentPath = parentPath.startsWith('/') ? parentPath : '/' + parentPath;
|
||
fullPath = normalizedParentPath + '/' + menu.path;
|
||
} else {
|
||
// 如果没有父路径但不是绝对路径,添加 '/' 前缀
|
||
fullPath = '/' + menu.path;
|
||
}
|
||
}
|
||
|
||
// 清理路径中的双斜杠
|
||
fullPath = fullPath.replace(/\/+/g, '/');
|
||
}
|
||
|
||
// 处理有meta信息且有标题的菜单项
|
||
if (menu.meta && menu.meta.title) {
|
||
// 获取图标
|
||
const iconKey = menu.meta.icon || 'document';
|
||
const icon = iconMap[iconKey] || Document;
|
||
|
||
// 获取颜色(循环使用预定义颜色)
|
||
const colorIndex = result.length % moduleColors.length;
|
||
const color = moduleColors[colorIndex];
|
||
|
||
// 构建当前菜单的层级链
|
||
const currentChain = [...parentChain];
|
||
if (menu.meta.title) {
|
||
currentChain.push(menu.meta.title);
|
||
}
|
||
|
||
// 确定一级目录名称 - 改进的层级识别逻辑
|
||
let firstLevelModuleName = '';
|
||
let menuPath = ''; // 用于显示层级路径
|
||
let moduleOrder = 0; // 一级目录的顺序
|
||
|
||
if (level === 0) {
|
||
// 当前就是一级目录
|
||
firstLevelModuleName = menu.meta.title;
|
||
menuPath = menu.meta.title;
|
||
moduleOrder = menuIndex; // 使用一级目录在API中的原始索引
|
||
} else {
|
||
// 从层级链中取第一个作为一级目录
|
||
firstLevelModuleName = currentChain[0] || '其他功能';
|
||
// 构建完整的层级路径(一级 > 二级 > 菜单)
|
||
menuPath = currentChain.join(' > ');
|
||
// 对于子菜单,使用父级的模块顺序
|
||
moduleOrder = Math.floor(menuIndex / 1000); // 简单的模块顺序推算
|
||
}
|
||
|
||
// 检查是否有可见的子菜单
|
||
const hasVisibleChildren = menu.children && menu.children.some((child: any) => !child.hidden);
|
||
|
||
// 检查是否为目录类型菜单
|
||
// 1. 组件为Layout或ParentView字符串
|
||
// 2. 组件为Vue对象且没有子菜单(可能是目录)
|
||
// 3. menuType为'M'(目录类型)
|
||
const isDirectoryMenu =
|
||
!isExternalLink &&
|
||
(menu.component === 'Layout' ||
|
||
menu.component === 'ParentView' ||
|
||
(typeof menu.component === 'object' && !hasVisibleChildren) ||
|
||
menu.menuType === 'M');
|
||
|
||
// 添加工作流菜单的详细调试信息
|
||
if (menu.meta?.title === '工作流' || menu.name === 'Workflow11616') {
|
||
console.log('=== 工作流菜单判断过程 ===');
|
||
console.log('hasVisibleChildren:', hasVisibleChildren);
|
||
console.log('isDirectoryMenu:', isDirectoryMenu);
|
||
console.log('menu.component:', menu.component);
|
||
console.log('menu.component类型:', typeof menu.component);
|
||
console.log('menu.menuType:', menu.menuType);
|
||
console.log('menu.children:', menu.children);
|
||
console.log('fullPath:', fullPath);
|
||
console.log('level:', level);
|
||
console.log('是否应该创建菜单项:', !hasVisibleChildren && !isDirectoryMenu && fullPath && fullPath !== '/');
|
||
console.log('完整菜单对象:', JSON.stringify(menu, null, 2));
|
||
}
|
||
|
||
// 目录菜单处理逻辑:如果是目录菜单且没有子菜单,直接跳过
|
||
if (isDirectoryMenu && !hasVisibleChildren) {
|
||
if (menu.meta?.title === '工作流' || menu.name === 'Workflow11616') {
|
||
console.log('=== 工作流菜单被跳过 ===');
|
||
console.log('原因: 是目录菜单且没有子菜单');
|
||
}
|
||
// 不创建菜单项,直接跳过
|
||
}
|
||
// 只有叶子节点(没有子菜单或子菜单都是隐藏的)且不是目录菜单才作为可点击的菜单项
|
||
else if ((!hasVisibleChildren && !isDirectoryMenu && fullPath && fullPath !== '/') || (isExternalLink && fullPath)) {
|
||
const menuAction: MenuAction = {
|
||
id: `menu-${menu.name || fullPath}`,
|
||
title: menu.meta.title,
|
||
description: menu.meta.title + '功能',
|
||
icon: icon,
|
||
color: color,
|
||
route: fullPath,
|
||
available: true,
|
||
parentName: firstLevelModuleName, // 使用一级目录名称进行分组
|
||
fullPath: menuPath, // 保存完整的层级路径用于显示
|
||
menuType: menu.menuType || 'C',
|
||
visible: menu.visible || '0',
|
||
status: menu.status || '0',
|
||
moduleOrder: moduleOrder, // 添加模块顺序
|
||
menuOrder: menuIndex, // 添加菜单在当前层级的顺序
|
||
orderNum: menu.orderNum || 0, // 添加菜单的原始排序号
|
||
logoUrl: getIconPath(iconKey), // 根据icon名称获取对应图标
|
||
isExternal: isExternalLink
|
||
};
|
||
|
||
// 添加调试日志来验证路径构建和排序
|
||
console.log(
|
||
`菜单项路径构建: ${menu.meta.title} - 原始path: ${menu.path}, 构建后fullPath: ${fullPath}, orderNum: ${menu.orderNum}, parentPath: ${parentPath}, level: ${level}`
|
||
);
|
||
|
||
result.push(menuAction);
|
||
} else if (menu.meta?.title === '工作流' || menu.name === 'Workflow11616') {
|
||
console.log('=== 工作流菜单被过滤掉 ===');
|
||
console.log('原因: 是目录菜单或没有有效路径');
|
||
}
|
||
}
|
||
|
||
// 递归处理子菜单 - 传递完整的层级链
|
||
if (menu.children && menu.children.length > 0) {
|
||
const newChain = [...parentChain];
|
||
if (menu.meta?.title) {
|
||
newChain.push(menu.meta.title);
|
||
}
|
||
|
||
// 构建子菜单的父路径 - 使用已经构建好的完整路径
|
||
let childParentPath = fullPath;
|
||
|
||
// 如果当前菜单是目录(没有组件或组件为Layout/ParentView),则传递fullPath作为父路径
|
||
// 否则传递当前菜单的路径
|
||
if (!menu.component || menu.component === 'Layout' || menu.component === 'ParentView') {
|
||
childParentPath = fullPath;
|
||
} else {
|
||
// 对于有具体组件的菜单,子菜单路径应该基于当前路径构建
|
||
childParentPath = fullPath;
|
||
}
|
||
|
||
// 递归处理,传递层级链和增加的层级
|
||
const childMenus = flattenMenus(menu.children, newChain, childParentPath, level + 1);
|
||
result.push(...childMenus);
|
||
}
|
||
});
|
||
|
||
return result;
|
||
};
|
||
|
||
// 按一级目录分组菜单 - 保持API原始顺序
|
||
const groupMenusByModule = (menus: MenuAction[]): MenuModule[] => {
|
||
const moduleMap = new Map<string, { menus: MenuAction[]; order: number }>();
|
||
|
||
menus.forEach((menu) => {
|
||
const moduleName = menu.parentName || '其他功能';
|
||
if (!moduleMap.has(moduleName)) {
|
||
moduleMap.set(moduleName, {
|
||
menus: [],
|
||
order: menu.moduleOrder || 999 // 使用菜单的模块顺序,默认排在最后
|
||
});
|
||
}
|
||
moduleMap.get(moduleName)!.menus.push(menu);
|
||
});
|
||
|
||
const modules: MenuModule[] = [];
|
||
let colorIndex = 0;
|
||
|
||
// 按模块的原始顺序排序,而不是按名称排序
|
||
const sortedModuleEntries = Array.from(moduleMap.entries()).sort((a, b) => {
|
||
return a[1].order - b[1].order;
|
||
});
|
||
|
||
sortedModuleEntries.forEach(([moduleName, moduleData]) => {
|
||
const moduleMenus = moduleData.menus;
|
||
|
||
// 过滤掉无效的模块
|
||
if (moduleMenus.length === 0) {
|
||
return;
|
||
}
|
||
|
||
// 按orderNum排序前的日志
|
||
console.log(
|
||
`${moduleName}模块排序前:`,
|
||
moduleMenus.map((m) => `${m.title}(${m.orderNum})`)
|
||
);
|
||
|
||
const sortedMenus = moduleMenus.sort((a, b) => (a.orderNum || 0) - (b.orderNum || 0));
|
||
|
||
// 按orderNum排序后的日志
|
||
console.log(
|
||
`${moduleName}模块排序后:`,
|
||
sortedMenus.map((m) => `${m.title}(${m.orderNum})`)
|
||
);
|
||
|
||
const module: MenuModule = {
|
||
id: `module-${moduleName}`,
|
||
title: moduleName,
|
||
description: `${moduleName}模块包含${moduleMenus.length}个功能`,
|
||
color: moduleColors[colorIndex % moduleColors.length],
|
||
// 菜单项按原始orderNum排序,确保显示顺序正确
|
||
menus: sortedMenus
|
||
};
|
||
modules.push(module);
|
||
colorIndex++;
|
||
});
|
||
|
||
return modules;
|
||
};
|
||
|
||
// 获取菜单数据 - 使用permissionStore
|
||
const loadMenuData = async () => {
|
||
try {
|
||
// 从权限store获取顶部栏菜单数据(包含更完整的结构)
|
||
const topbarRoutes = permissionStore.getTopbarRoutes();
|
||
|
||
console.log('获取到的顶部栏路由数据:', topbarRoutes);
|
||
|
||
// 如果顶部栏数据为空,尝试获取侧边栏数据
|
||
const routesToProcess = topbarRoutes && topbarRoutes.length > 0 ? topbarRoutes : permissionStore.getSidebarRoutes();
|
||
|
||
// 过滤掉系统路由
|
||
const dynamicRoutes = routesToProcess.filter(
|
||
(route) =>
|
||
route.path !== '/' && route.path !== '/index' && route.path !== '/user' && !route.path.startsWith('/redirect') && route.hidden !== true
|
||
);
|
||
|
||
console.log('处理的动态路由数据:', dynamicRoutes);
|
||
|
||
// 扁平化菜单数据
|
||
const flatMenus = flattenMenus(dynamicRoutes);
|
||
allMenuActions.value = flatMenus;
|
||
|
||
// 按模块分组
|
||
const modules = groupMenusByModule(flatMenus);
|
||
menuModules.value = modules;
|
||
|
||
console.log('菜单数据处理完成', { flatMenus, modules });
|
||
} catch (error) {
|
||
console.error('获取菜单数据失败:', error);
|
||
ElMessage.error('获取菜单数据失败');
|
||
}
|
||
};
|
||
|
||
// 处理快捷功能点击
|
||
const handleQuickAction = (action: any) => {
|
||
// 检查功能是否可用
|
||
if (!action.available) {
|
||
ElMessage.warning(`${action.title}功能正在开发中,敬请期待!`);
|
||
return;
|
||
}
|
||
|
||
console.log('action', action);
|
||
|
||
// 外链菜单:在新标签页直接打开原始地址,不拼接
|
||
if (action.isExternal) {
|
||
window.open(action.route, '_blank');
|
||
ElMessage.success(`正在打开${action.title}...`);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 使用路由跳转,系统会自动在tab页中打开
|
||
router
|
||
.push(action.route)
|
||
.then(() => {
|
||
ElMessage.success(`正在打开${action.title}...`);
|
||
})
|
||
.catch((error) => {
|
||
console.error('路由跳转失败:', error);
|
||
// 如果路由不存在,显示提示信息
|
||
if (error.name === 'NavigationFailure') {
|
||
ElMessage.warning(`${action.title}功能正在开发中,敬请期待!`);
|
||
} else {
|
||
ElMessage.error(`打开${action.title}失败,请稍后重试`);
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('快捷功能点击处理失败:', error);
|
||
ElMessage.error(`打开${action.title}失败,请稍后重试`);
|
||
}
|
||
};
|
||
|
||
// 组件挂载后加载数据
|
||
onMounted(async () => {
|
||
// 等待权限数据加载完成,如果还没有数据则等待一段时间
|
||
const checkAndLoadMenuData = async () => {
|
||
let retries = 0;
|
||
const maxRetries = 10;
|
||
|
||
while (retries < maxRetries) {
|
||
const sidebarRoutes = permissionStore.getSidebarRoutes();
|
||
|
||
if (sidebarRoutes && sidebarRoutes.length > 0) {
|
||
// 权限数据已加载,可以处理菜单
|
||
await loadMenuData();
|
||
console.log('首页菜单数据加载完成');
|
||
return;
|
||
}
|
||
|
||
// 等待100ms后重试
|
||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||
retries++;
|
||
}
|
||
|
||
console.warn('权限数据加载超时,使用空菜单');
|
||
menuModules.value = [];
|
||
};
|
||
|
||
await checkAndLoadMenuData();
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.home {
|
||
// min-height: calc(100vh - 200px);
|
||
// background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||
}
|
||
|
||
// 上半部分:轮播图样式
|
||
.carousel-section {
|
||
padding: 0 10px;
|
||
margin-bottom: 20px;
|
||
margin-top: 20px;
|
||
// background: rgba(255, 255, 255, 0.1);
|
||
backdrop-filter: blur(5px);
|
||
}
|
||
|
||
.campus-carousel {
|
||
// max-width: 1200px;
|
||
margin: 0 auto;
|
||
border-radius: 16px;
|
||
overflow: hidden;
|
||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||
|
||
:deep(.el-carousel__item) {
|
||
border-radius: 16px;
|
||
}
|
||
}
|
||
|
||
.carousel-item {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
overflow: hidden;
|
||
border-radius: 16px;
|
||
}
|
||
|
||
.carousel-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.carousel-item:hover .carousel-image {
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.carousel-overlay {
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
// background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
|
||
color: white;
|
||
padding: 40px 30px 30px;
|
||
text-align: left;
|
||
}
|
||
|
||
.carousel-title {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
margin: 0 0 10px 0;
|
||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
|
||
}
|
||
|
||
.carousel-description {
|
||
font-size: 16px;
|
||
margin: 0;
|
||
opacity: 0.9;
|
||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
|
||
line-height: 1.5;
|
||
}
|
||
|
||
// 下半部分:原有样式
|
||
.construction-container {
|
||
width: 100%;
|
||
max-width: 800px;
|
||
padding: 40px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.construction-content {
|
||
text-align: center;
|
||
background: rgba(255, 255, 255, 0.95);
|
||
backdrop-filter: blur(10px);
|
||
border-radius: 20px;
|
||
padding: 60px 40px;
|
||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
}
|
||
|
||
.system-title {
|
||
font-size: 28px;
|
||
font-weight: 600;
|
||
color: #2c3e50;
|
||
margin-bottom: 40px;
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
.construction-icon {
|
||
margin-bottom: 30px;
|
||
animation: float 3s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes float {
|
||
0%,
|
||
100% {
|
||
transform: translateY(0px);
|
||
}
|
||
50% {
|
||
transform: translateY(-10px);
|
||
}
|
||
}
|
||
|
||
.construction-text {
|
||
margin-bottom: 40px;
|
||
|
||
h2 {
|
||
font-size: 24px;
|
||
font-weight: 500;
|
||
color: #409eff;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.description {
|
||
font-size: 16px;
|
||
color: #606266;
|
||
margin-bottom: 10px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.sub-description {
|
||
font-size: 14px;
|
||
color: #909399;
|
||
line-height: 1.5;
|
||
}
|
||
}
|
||
|
||
.progress-section {
|
||
margin-bottom: 30px;
|
||
|
||
.progress-label {
|
||
font-size: 14px;
|
||
color: #606266;
|
||
margin-bottom: 10px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.progress-bar {
|
||
max-width: 200px;
|
||
margin: 0 auto;
|
||
}
|
||
}
|
||
|
||
.completion-info {
|
||
margin-bottom: 40px;
|
||
|
||
.el-tag {
|
||
padding: 12px 20px;
|
||
font-size: 14px;
|
||
|
||
.tag-icon {
|
||
margin-right: 8px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.contact-section {
|
||
max-width: 200px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.contact-card {
|
||
border-radius: 12px;
|
||
border: none;
|
||
|
||
.card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-weight: 500;
|
||
color: #409eff;
|
||
}
|
||
}
|
||
|
||
.contact-content {
|
||
p {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-bottom: 12px;
|
||
font-size: 14px;
|
||
color: #606266;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.el-icon {
|
||
color: #409eff;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 菜单功能区域样式
|
||
.menu-modules-section {
|
||
padding: 20px 10px;
|
||
background: rgba(255, 255, 255, 0.95);
|
||
backdrop-filter: blur(10px);
|
||
border-radius: 16px;
|
||
margin-bottom: 20px;
|
||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
margin-left: 10px;
|
||
margin-right: 10px;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
color: #2c3e50;
|
||
margin-bottom: 25px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding-bottom: 15px;
|
||
border-bottom: 2px solid #f1f2f6;
|
||
|
||
.section-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.section-icon-img {
|
||
width: 28px;
|
||
height: 28px;
|
||
object-fit: contain;
|
||
}
|
||
|
||
.function-count {
|
||
font-size: 14px;
|
||
font-weight: 400;
|
||
color: #909399;
|
||
margin-left: 10px;
|
||
}
|
||
}
|
||
|
||
.module-group {
|
||
margin-bottom: 35px;
|
||
padding: 20px;
|
||
background: rgba(255, 255, 255, 0.8);
|
||
border-radius: 12px;
|
||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||
transition: all 0.3s ease;
|
||
|
||
&:hover {
|
||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
.module-header {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
margin-bottom: 20px;
|
||
padding-bottom: 15px;
|
||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.module-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
|
||
.module-icon-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.module-icon-img {
|
||
width: 20px;
|
||
height: 20px;
|
||
object-fit: contain;
|
||
}
|
||
|
||
.module-count-tag {
|
||
margin-left: 10px;
|
||
font-weight: 400;
|
||
}
|
||
}
|
||
|
||
.module-description {
|
||
font-size: 14px;
|
||
color: #606266;
|
||
line-height: 1.5;
|
||
padding-left: 28px; // 与图标对齐
|
||
}
|
||
|
||
.module-content {
|
||
margin-top: 15px;
|
||
}
|
||
|
||
// 新增弹性布局样式
|
||
.module-content-flex {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 20px;
|
||
margin-top: 15px;
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.menu-card-wrapper {
|
||
flex: 0 0 calc((100% - 140px) / 8); // 每行8个,精确计算:(100% - 7个间隔*20px) / 8
|
||
min-width: 100px;
|
||
margin-bottom: 20px;
|
||
|
||
@media (max-width: 1600px) {
|
||
flex: 0 0 calc((100% - 120px) / 7); // 中大屏幕每行7个
|
||
}
|
||
|
||
@media (max-width: 1400px) {
|
||
flex: 0 0 calc((100% - 100px) / 6); // 中等屏幕每行6个
|
||
}
|
||
|
||
@media (max-width: 1200px) {
|
||
flex: 0 0 calc((100% - 80px) / 5); // 小屏幕每行5个
|
||
}
|
||
|
||
@media (max-width: 992px) {
|
||
flex: 0 0 calc((100% - 60px) / 4); // 平板每行4个
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
flex: 0 0 calc((100% - 40px) / 3); // 移动端每行3个
|
||
}
|
||
|
||
@media (max-width: 600px) {
|
||
flex: 0 0 calc((100% - 20px) / 2); // 小手机每行2个
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
flex: 0 0 calc(100% - 20px); // 超小屏幕每行1个
|
||
}
|
||
}
|
||
|
||
.menu-action-card {
|
||
border-radius: 12px;
|
||
border: none;
|
||
transition: all 0.3s ease;
|
||
cursor: pointer;
|
||
height: 110px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
&:hover {
|
||
transform: translateY(-5px);
|
||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
&.disabled {
|
||
cursor: not-allowed;
|
||
opacity: 0.6;
|
||
|
||
&:hover {
|
||
transform: none;
|
||
box-shadow: none;
|
||
}
|
||
}
|
||
}
|
||
|
||
.menu-action {
|
||
text-align: center;
|
||
padding: 10px 8px;
|
||
width: 100%;
|
||
}
|
||
|
||
// 原图标样式(保留以防需要)
|
||
.menu-icon {
|
||
font-size: 36px;
|
||
margin-bottom: 12px;
|
||
display: block;
|
||
}
|
||
|
||
// 新增logo图片样式
|
||
.menu-logo-wrapper {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
height: 36px;
|
||
}
|
||
|
||
.menu-logo {
|
||
width: 36px;
|
||
height: 36px;
|
||
object-fit: contain;
|
||
border-radius: 6px;
|
||
transition: all 0.3s ease;
|
||
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.1));
|
||
|
||
&:hover {
|
||
transform: scale(1.1);
|
||
filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.15));
|
||
}
|
||
}
|
||
|
||
.loading-state {
|
||
text-align: center;
|
||
padding: 60px 20px;
|
||
|
||
.loading-tips {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
margin-top: 8px;
|
||
}
|
||
}
|
||
|
||
.action-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 4px;
|
||
display: block;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.action-desc {
|
||
font-size: 11px;
|
||
color: #666;
|
||
display: block;
|
||
line-height: 1.4;
|
||
|
||
&.disabled {
|
||
color: #c0c4cc;
|
||
}
|
||
}
|
||
|
||
// .action-path 样式已移除,因为不再显示层级路径
|
||
|
||
.action-title.disabled {
|
||
color: #c0c4cc;
|
||
}
|
||
|
||
.status-tag {
|
||
margin-top: 8px;
|
||
display: inline-block;
|
||
}
|
||
|
||
// 响应式设计
|
||
@media (max-width: 768px) {
|
||
.carousel-section {
|
||
padding: 20px 10px;
|
||
}
|
||
|
||
.campus-carousel {
|
||
height: 300px !important;
|
||
}
|
||
|
||
.carousel-overlay {
|
||
padding: 30px 20px 20px;
|
||
}
|
||
|
||
.carousel-title {
|
||
font-size: 20px;
|
||
}
|
||
|
||
.carousel-description {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.menu-modules-section {
|
||
padding: 15px 10px;
|
||
margin: 10px;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 20px;
|
||
margin-bottom: 20px;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
text-align: center;
|
||
|
||
.function-count {
|
||
margin-left: 0;
|
||
}
|
||
}
|
||
|
||
.module-group {
|
||
padding: 15px;
|
||
margin-bottom: 25px;
|
||
}
|
||
|
||
.module-header {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.module-title {
|
||
font-size: 16px;
|
||
justify-content: center;
|
||
text-align: center;
|
||
}
|
||
|
||
.module-description {
|
||
text-align: center;
|
||
padding-left: 0;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.module-content-flex {
|
||
gap: 15px;
|
||
}
|
||
|
||
.menu-card-wrapper {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.menu-action-card {
|
||
height: 100px;
|
||
}
|
||
|
||
.menu-action {
|
||
padding: 8px 5px;
|
||
}
|
||
|
||
.menu-logo-wrapper {
|
||
height: 30px;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.menu-logo {
|
||
width: 30px;
|
||
height: 30px;
|
||
}
|
||
|
||
.action-title {
|
||
font-size: 13px;
|
||
}
|
||
|
||
.action-desc {
|
||
font-size: 10px;
|
||
}
|
||
|
||
.loading-state {
|
||
padding: 40px 10px;
|
||
}
|
||
}
|
||
|
||
// 深色主题适配
|
||
html.dark {
|
||
.carousel-section {
|
||
background: rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.menu-modules-section {
|
||
background: rgba(30, 30, 30, 0.95);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.section-title {
|
||
color: #e5eaf3;
|
||
border-bottom: 2px solid #404040;
|
||
}
|
||
|
||
.module-group {
|
||
background: rgba(40, 40, 40, 0.8);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
|
||
&:hover {
|
||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
|
||
}
|
||
}
|
||
|
||
.module-header {
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.module-title {
|
||
color: #000000 !important;
|
||
}
|
||
|
||
.module-description {
|
||
color: #a3a6ad;
|
||
}
|
||
|
||
.menu-action-card {
|
||
background: rgba(50, 50, 50, 0.8);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
|
||
&:hover {
|
||
background: rgba(60, 60, 60, 0.9);
|
||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.3);
|
||
}
|
||
}
|
||
|
||
.action-title {
|
||
color: #e5eaf3;
|
||
|
||
&.disabled {
|
||
color: #6c6e72;
|
||
}
|
||
}
|
||
|
||
.action-desc {
|
||
color: #a3a6ad;
|
||
|
||
&.disabled {
|
||
color: #6c6e72;
|
||
}
|
||
}
|
||
|
||
.construction-content {
|
||
background: rgba(30, 30, 30, 0.95);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.system-title {
|
||
color: #e5eaf3;
|
||
}
|
||
|
||
.construction-text {
|
||
.description {
|
||
color: #a3a6ad;
|
||
}
|
||
|
||
.sub-description {
|
||
color: #6c6e72;
|
||
}
|
||
}
|
||
|
||
.progress-section .progress-label {
|
||
color: #a3a6ad;
|
||
}
|
||
|
||
.contact-content p {
|
||
color: #a3a6ad;
|
||
}
|
||
}
|
||
</style>
|