路由更新

This commit is contained in:
2025-10-17 12:05:04 +08:00
parent 0811af6d03
commit edadcc72ca
15 changed files with 456 additions and 557 deletions

View File

@@ -60,33 +60,34 @@ INSERT INTO `tb_sys_role_permission` (id, role_id, permission_id, creator, creat
-- 插入前端菜单数据 -- 插入前端菜单数据
INSERT INTO `tb_sys_menu` (id, menu_id, name, parent_id, url, component, icon, order_num, type, layout, creator, create_time) VALUES INSERT INTO `tb_sys_menu` (id, menu_id, name, parent_id, url, component, icon, order_num, type, layout, creator, create_time) VALUES
('100', 'menu_home', '首页', NULL, '/home', 'home/HomeView', 'el-icon-house', 1, 1, 'NavigationLayout', '1', now()),
-- 资源中心 -- 资源中心
('200', 'menu_resource_center', '资源中心', NULL, '/resource-center', 'resource-center/ResourceCenterView', 'el-icon-folder-opened', 2, 1, 'BasicLayout', '1', now()), ('200', 'menu_resource_center', '资源中心', NULL, '/resource-center', 'resource-center/ResourceCenterView', 'el-icon-folder-opened', 2, 1, 'NavigationLayout', '1', now()),
('201', 'menu_party_history', '党史学习', 'menu_resource_center', '/resource-center/party-history', 'resource-center/PartyHistoryView', 'el-icon-trophy', 1, 1, 'BasicLayout', '1', now()), ('201', 'menu_party_history', '党史学习', 'menu_resource_center', '/resource-center/party-history', 'resource-center/PartyHistoryView', 'el-icon-trophy', 1, 1, 'NavigationLayout', '1', now()),
('202', 'menu_leader_speech', '领导讲话', 'menu_resource_center', '/resource-center/leader-speech', 'resource-center/LeaderSpeechView', 'el-icon-microphone', 2, 1, 'BasicLayout', '1', now()), ('202', 'menu_leader_speech', '领导讲话', 'menu_resource_center', '/resource-center/leader-speech', 'resource-center/LeaderSpeechView', 'el-icon-microphone', 2, 1, 'NavigationLayout', '1', now()),
('203', 'menu_policy_interpretation', '政策解读', 'menu_resource_center', '/resource-center/policy-interpretation', 'resource-center/PolicyInterpretationView', 'el-icon-document', 3, 1, 'BasicLayout', '1', now()), ('203', 'menu_policy_interpretation', '政策解读', 'menu_resource_center', '/resource-center/policy-interpretation', 'resource-center/PolicyInterpretationView', 'el-icon-document', 3, 1, 'NavigationLayout', '1', now()),
('204', 'menu_red_classic', '红色经典', 'menu_resource_center', '/resource-center/red-classic', 'resource-center/RedClassicView', 'el-icon-star-on', 4, 1, 'BasicLayout', '1', now()), ('204', 'menu_red_classic', '红色经典', 'menu_resource_center', '/resource-center/red-classic', 'resource-center/RedClassicView', 'el-icon-star-on', 4, 1, 'NavigationLayout', '1', now()),
('205', 'menu_special_report', '专题报告', 'menu_resource_center', '/resource-center/special-report', 'resource-center/SpecialReportView', 'el-icon-document-copy', 5, 1, 'BasicLayout', '1', now()), ('205', 'menu_special_report', '专题报告', 'menu_resource_center', '/resource-center/special-report', 'resource-center/SpecialReportView', 'el-icon-document-copy', 5, 1, 'NavigationLayout', '1', now()),
('206', 'menu_world_case', '思政案例', 'menu_resource_center', '/resource-center/world-case', 'resource-center/WorldCaseView', 'el-icon-collection', 6, 1, 'BasicLayout', '1', now()), ('206', 'menu_world_case', '思政案例', 'menu_resource_center', '/resource-center/world-case', 'resource-center/WorldCaseView', 'el-icon-collection', 6, 1, 'NavigationLayout', '1', now()),
-- 学习计划 -- 学习计划
('300', 'menu_study_plan', '学习计划', NULL, '/study-plan', 'study-plan/StudyPlanView', 'el-icon-reading', 3, 1, 'BasicLayout', '1', now()), ('300', 'menu_study_plan', '学习计划', NULL, '/study-plan', 'study-plan/StudyPlanView', 'el-icon-reading', 3, 1, 'NavigationLayout', '1', now()),
('301', 'menu_study_tasks', '学习任务', 'menu_study_plan', '/study-plan/tasks', 'study-plan/StudyTasksView', 'el-icon-s-order', 1, 1, 'BasicLayout', '1', now()), ('301', 'menu_study_tasks', '学习任务', 'menu_study_plan', '/study-plan/tasks', 'study-plan/StudyTasksView', 'el-icon-s-order', 1, 1, 'NavigationLayout', '1', now()),
('302', 'menu_course_center', '课程中心', 'menu_study_plan', '/study-plan/course', 'study-plan/CourseCenterView', 'el-icon-video-play', 2, 1, 'BasicLayout', '1', now()), ('302', 'menu_course_center', '课程中心', 'menu_study_plan', '/study-plan/course', 'study-plan/CourseCenterView', 'el-icon-video-play', 2, 1, 'NavigationLayout', '1', now()),
-- 个人中心 -- 个人中心
('400', 'menu_user_center', '个人中心', NULL, '/user-center', 'user-center/UserCenterView', 'el-icon-user', 4, 1, 'BasicLayout', '1', now()), ('400', 'menu_user_center', '个人中心', NULL, '/user-center', 'user-center/UserCenterView', 'el-icon-user', 4, 1, 'NavigationLayout', '1', now()),
('401', 'menu_learning_records', '学习记录', 'menu_user_center', '/user-center/learning-records', 'user-center/LearningRecordsView', 'el-icon-document', 1, 1, 'BasicLayout', '1', now()), ('401', 'menu_learning_records', '学习记录', 'menu_user_center', '/user-center/learning-records', 'user-center/LearningRecordsView', 'el-icon-document', 1, 1, 'NavigationLayout', '1', now()),
('402', 'menu_my_favorites', '我的收藏', 'menu_user_center', '/user-center/favorites', 'user-center/MyFavoritesView', 'el-icon-star-on', 2, 1, 'BasicLayout', '1', now()), ('402', 'menu_my_favorites', '我的收藏', 'menu_user_center', '/user-center/favorites', 'user-center/MyFavoritesView', 'el-icon-star-on', 2, 1, 'NavigationLayout', '1', now()),
('403', 'menu_my_achievements', '我的成就', 'menu_user_center', '/user-center/achievements', 'user-center/MyAchievementsView', 'el-icon-trophy', 3, 1, 'BasicLayout', '1', now()), ('403', 'menu_my_achievements', '我的成就', 'menu_user_center', '/user-center/achievements', 'user-center/MyAchievementsView', 'el-icon-trophy', 3, 1, 'NavigationLayout', '1', now()),
-- 账号中心 -- 账号中心
('500', 'menu_profile', '账号中心', NULL, '/profile', 'profile/ProfileView', 'el-icon-user-solid', 5, 1, 'BasicLayout', '1', now()), ('500', 'menu_profile', '账号中心', NULL, '/profile', 'profile/ProfileView', 'el-icon-user-solid', 5, 1, 'NavigationLayout', '1', now()),
('501', 'menu_personal_info', '个人信息', 'menu_profile', '/profile/personal-info', 'profile/PersonalInfoView', 'el-icon-user', 1, 1, 'BasicLayout', '1', now()), ('501', 'menu_personal_info', '个人信息', 'menu_profile', '/profile/personal-info', 'profile/PersonalInfoView', 'el-icon-user', 1, 1, 'NavigationLayout', '1', now()),
('502', 'menu_account_settings', '账号设置', 'menu_profile', '/profile/account-settings', 'profile/AccountSettingsView', 'el-icon-setting', 2, 1, 'BasicLayout', '1', now()), ('502', 'menu_account_settings', '账号设置', 'menu_profile', '/profile/account-settings', 'profile/AccountSettingsView', 'el-icon-setting', 2, 1, 'NavigationLayout', '1', now()),
-- 智能体模块 -- 智能体模块
('600', 'menu_ai_assistant', '智能体模块', NULL, '/ai-assistant', 'ai-assistant/AIAssistantView', 'el-icon-cpu', 6, 1, 'BasicLayout', '1', now()); ('600', 'menu_ai_assistant', '智能体模块', NULL, '/ai-assistant', 'ai-assistant/AIAssistantView', 'el-icon-cpu', 6, 1, 'NavigationLayout', '1', now());
-- 插入后端管理菜单数据 -- 插入后端管理菜单数据
INSERT INTO `tb_sys_menu` (id, menu_id, name, parent_id, url, component, icon, order_num, type, layout, creator, create_time) VALUES INSERT INTO `tb_sys_menu` (id, menu_id, name, parent_id, url, component, icon, order_num, type, layout, creator, create_time) VALUES

View File

@@ -0,0 +1,357 @@
<template>
<aside class="floating-sidebar" :class="{ collapsed: collapsed, [type]: true }">
<!-- 折叠按钮 -->
<div class="sidebar-toggle-btn" @click="$emit('toggle')">
<i class="toggle-icon">{{ collapsed ? '▶' : '◀' }}</i>
</div>
<!-- 侧边栏内容 -->
<div class="sidebar-content" v-if="!collapsed">
<!-- 标题 -->
<div class="sidebar-header" v-if="title">
<h3 class="sidebar-title">{{ title }}</h3>
</div>
<!-- 菜单列表 -->
<nav class="sidebar-nav">
<div
v-for="menu in menus"
:key="menu.menuID"
class="sidebar-item"
:class="{ active: isActive(menu), 'has-children': hasChildren(menu) }"
>
<div class="sidebar-link" @click="handleClick(menu)">
<span class="link-text">{{ menu.name }}</span>
<i v-if="hasChildren(menu)" class="arrow-icon"></i>
</div>
<!-- 子菜单 -->
<div v-if="hasChildren(menu) && isExpanded(menu)" class="sidebar-submenu">
<div
v-for="child in menu.children"
:key="child.menuID"
class="submenu-item"
:class="{ active: isActive(child) }"
@click="handleClick(child)"
>
<span class="submenu-text">{{ child.name }}</span>
</div>
</div>
</div>
</nav>
</div>
<!-- 折叠状态的图标 -->
<div class="sidebar-icons" v-else>
<div
v-for="menu in menus"
:key="menu.menuID"
class="icon-item"
:class="{ active: isActive(menu) }"
:title="menu.name"
@click="handleClick(menu)"
>
<span class="icon-text">{{ menu.name?.charAt(0) }}</span>
</div>
</div>
</aside>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import type { SysMenu } from '@/types';
// Props
interface Props {
menus: SysMenu[];
collapsed?: boolean;
title?: string;
type?: 'nav' | 'sidebar';
activePath?: string;
}
const props = withDefaults(defineProps<Props>(), {
collapsed: false,
type: 'sidebar'
});
// Emits
const emit = defineEmits<{
'toggle': [];
'menu-click': [menu: SysMenu];
}>();
// 展开的菜单ID列表
const expandedMenus = ref<Set<string>>(new Set());
// 检查菜单是否有子菜单
function hasChildren(menu: SysMenu): boolean {
return !!(menu.children && menu.children.length > 0);
}
// 检查菜单是否激活
function isActive(menu: SysMenu): boolean {
if (!menu.url) return false;
return props.activePath === menu.url;
}
// 检查菜单是否展开
function isExpanded(menu: SysMenu): boolean {
return menu.menuID ? expandedMenus.value.has(menu.menuID) : false;
}
// 处理点击
function handleClick(menu: SysMenu) {
if (hasChildren(menu)) {
// 切换展开状态
if (menu.menuID) {
if (expandedMenus.value.has(menu.menuID)) {
expandedMenus.value.delete(menu.menuID);
} else {
expandedMenus.value.add(menu.menuID);
}
}
}
// 触发点击事件
emit('menu-click', menu);
}
</script>
<style lang="scss" scoped>
.floating-sidebar {
background: white;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
flex-shrink: 0;
display: flex;
flex-direction: column;
position: relative;
&.sidebar {
width: 260px;
&.collapsed {
width: 64px;
}
}
&.nav {
width: 200px;
&.collapsed {
width: 56px;
}
}
}
.sidebar-toggle-btn {
width: 24px;
height: 48px;
background: white;
border: 1px solid #e8e8e8;
border-radius: 0 12px 12px 0;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
z-index: 10;
&:hover {
background: #f0f2f5;
border-color: #C62828;
}
.toggle-icon {
font-size: 12px;
color: #666;
font-style: normal;
}
}
.sidebar-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.sidebar-header {
padding: 20px 16px 16px;
border-bottom: 1px solid #f0f0f0;
.sidebar-title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #141F38;
}
}
.sidebar-nav {
flex: 1;
overflow-y: auto;
padding: 8px 0;
/* 滚动条样式 */
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: #f5f5f5;
}
&::-webkit-scrollbar-thumb {
background: #ddd;
border-radius: 3px;
&:hover {
background: #bbb;
}
}
}
.sidebar-item {
margin: 4px 8px;
border-radius: 4px;
overflow: hidden;
transition: all 0.3s;
&.active {
background: #fff1f0;
.sidebar-link {
color: #C62828;
font-weight: 500;
}
}
&:hover {
background: #f5f5f5;
}
}
.sidebar-link {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
color: #333;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
user-select: none;
.link-text {
flex: 1;
}
.arrow-icon {
font-size: 10px;
font-style: normal;
transition: transform 0.3s;
color: #999;
}
&:hover {
color: #C62828;
}
}
.sidebar-submenu {
background: #fafafa;
margin: 0 8px 4px;
border-radius: 4px;
overflow: hidden;
}
.submenu-item {
padding: 10px 16px 10px 32px;
color: #666;
font-size: 13px;
cursor: pointer;
transition: all 0.3s;
user-select: none;
&:hover {
background: #f0f0f0;
color: #C62828;
}
&.active {
background: #fff1f0;
color: #C62828;
font-weight: 500;
}
.submenu-text {
display: block;
}
}
.sidebar-icons {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 16px 8px;
gap: 8px;
overflow-y: auto;
}
.icon-item {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
background: #f5f5f5;
&:hover {
background: #e8e8e8;
}
&.active {
background: #C62828;
.icon-text {
color: white;
}
}
.icon-text {
font-size: 16px;
font-weight: 500;
color: #666;
user-select: none;
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.floating-sidebar {
&.sidebar {
width: 200px;
&.collapsed {
width: 56px;
}
}
&.nav {
width: 180px;
&.collapsed {
width: 50px;
}
}
}
}
</style>

View File

@@ -3,7 +3,7 @@
<div class="nav-container"> <div class="nav-container">
<!-- Logo区域 --> <!-- Logo区域 -->
<div class="nav-logo"> <div class="nav-logo">
<img src="@/assets/imgs/logo-icon.svg" alt="Logo" class="logo-icon" /> <img src="../../assets/imgs/logo-icon.svg" alt="Logo" class="logo-icon" />
<span class="logo-text">红色思政学习平台</span> <span class="logo-text">红色思政学习平台</span>
</div> </div>
@@ -23,13 +23,12 @@
</div> </div>
<!-- 下拉菜单 --> <!-- 下拉菜单 -->
<Teleport to="body"> <Teleport to="body" v-if="hasNavigationChildren(menu)">
<div <div
v-if="hasNavigationChildren(menu)"
class="dropdown-menu" class="dropdown-menu"
:class="{ show: activeDropdown === menu.menuID }" :class="{ show: activeDropdown === menu.menuID }"
:style="getDropdownPosition(menu)" :style="getDropdownPosition(menu)"
@mouseenter="handleMouseEnter(menu)" @mouseenter="() => { if (menu.menuID) activeDropdown = menu.menuID }"
@mouseleave="handleMouseLeave" @mouseleave="handleMouseLeave"
> >
<div <div
@@ -58,7 +57,7 @@
@keyup.enter="handleSearch" @keyup.enter="handleSearch"
/> />
<div class="search-icon"> <div class="search-icon">
<img src="@/assets/imgs/search-icon.svg" alt="搜索" /> <img src="../../assets/imgs/search-icon.svg" alt="搜索" />
</div> </div>
</div> </div>
</div> </div>
@@ -152,9 +151,11 @@ function handleMouseEnter(menu: SysMenu, event?: MouseEvent) {
activeDropdown.value = menu.menuID || null; activeDropdown.value = menu.menuID || null;
// //
const target = event?.currentTarget as HTMLElement; if (event && menu.menuID) {
if (target && menu.menuID) { const target = event.currentTarget as HTMLElement;
const rect = target.getBoundingClientRect(); const navLink = target.querySelector('.nav-link') as HTMLElement;
const rect = (navLink || target).getBoundingClientRect();
dropdownPositions.value[menu.menuID] = { dropdownPositions.value[menu.menuID] = {
left: rect.left, left: rect.left,
top: rect.bottom, top: rect.bottom,
@@ -167,11 +168,15 @@ function handleMouseEnter(menu: SysMenu, event?: MouseEvent) {
// //
function getDropdownPosition(menu: SysMenu) { function getDropdownPosition(menu: SysMenu) {
const menuID = menu.menuID; const menuID = menu.menuID;
if (!menuID || !dropdownPositions.value[menuID]) { const pos = menuID && dropdownPositions.value[menuID];
return {};
if (!pos) {
return {
position: 'fixed' as const,
visibility: 'hidden' as const
};
} }
const pos = dropdownPositions.value[menuID];
return { return {
position: 'fixed' as const, position: 'fixed' as const,
left: `${pos.left}px`, left: `${pos.left}px`,
@@ -356,15 +361,16 @@ function handleLogout() {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transform: translateY(-10px); transform: scaleY(0);
transition: all 0.3s; transform-origin: top center;
transition: opacity 0.2s, transform 0.2s, visibility 0.2s;
z-index: 10000; z-index: 10000;
pointer-events: none; pointer-events: none;
&.show { &.show {
opacity: 1; opacity: 1;
visibility: visible; visibility: visible;
transform: translateY(0); transform: scaleY(1);
pointer-events: auto; pointer-events: auto;
} }
} }

View File

@@ -0,0 +1,6 @@
export { default as Breadcrumb } from './Breadcrumb.vue';
export { default as FloatingSidebar } from './FloatingSidebar.vue';
export { default as MenuItem } from './MenuItem.vue';
export { default as MenuNav } from './MenuNav.vue';
export { default as TopNavigation } from './TopNavigation.vue';
export { default as UserDropdown } from './UserDropdown.vue';

View File

@@ -0,0 +1,3 @@
// 导出 base 基础组件
export * from './base';

View File

@@ -57,12 +57,7 @@ import { useRoute, useRouter } from "vue-router";
import { useStore } from "vuex"; import { useStore } from "vuex";
import type { SysMenu } from "@/types"; import type { SysMenu } from "@/types";
import { getMenuPath } from "@/utils/route-generator"; import { getMenuPath } from "@/utils/route-generator";
// @ts-ignore - Vue 3.5 defineOptions支持 import { MenuNav, Breadcrumb, UserDropdown } from "@/components/base";
import MenuNav from "@/components/MenuNav.vue";
// @ts-ignore - Vue 3.5 组件导入兼容性
import Breadcrumb from "@/components/Breadcrumb.vue";
// @ts-ignore - Vue 3.5 组件导入兼容性
import UserDropdown from "@/components/UserDropdown.vue";
// 响应式状态 // 响应式状态
const sidebarCollapsed = ref(false); const sidebarCollapsed = ref(false);

View File

@@ -6,9 +6,9 @@
<!-- 主内容区域 --> <!-- 主内容区域 -->
<div class="layout-content"> <div class="layout-content">
<!-- 面包屑 --> <!-- 面包屑 -->
<div class="breadcrumb-wrapper" v-if="breadcrumbItems.length > 0"> <!-- <div class="breadcrumb-wrapper" v-if="breadcrumbItems.length > 0">
<Breadcrumb :items="breadcrumbItems" /> <Breadcrumb :items="breadcrumbItems" />
</div> </div> -->
<!-- 侧边栏和内容 --> <!-- 侧边栏和内容 -->
<div class="content-wrapper" v-if="hasSidebarMenus"> <div class="content-wrapper" v-if="hasSidebarMenus">
@@ -50,12 +50,7 @@ import { useStore } from 'vuex';
import type { SysMenu } from '@/types'; import type { SysMenu } from '@/types';
import { MenuType } from '@/types/enums'; import { MenuType } from '@/types/enums';
import { getMenuPath } from '@/utils/route-generator'; import { getMenuPath } from '@/utils/route-generator';
// @ts-ignore - Vue 3.5 组件导入兼容性 import { TopNavigation, MenuNav, Breadcrumb } from '@/components';
import TopNavigation from '@/components/TopNavigation.vue';
// @ts-ignore - Vue 3.5 组件导入兼容性
import MenuNav from '@/components/MenuNav.vue';
// @ts-ignore - Vue 3.5 组件导入兼容性
import Breadcrumb from '@/components/Breadcrumb.vue';
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();

View File

@@ -57,23 +57,23 @@ export const routes: Array<RouteRecordRaw> = [
], ],
}, },
// 首页(显示在导航栏) // 首页(显示在导航栏)
{ // {
path: "/home", // path: "/home",
component: () => import("@/layouts/NavigationLayout.vue"), // component: () => import("@/layouts/NavigationLayout.vue"),
children: [ // children: [
{ // {
path: "", // path: "",
name: "Home", // name: "Home",
component: () => import("@/views/HomeView.vue"), // component: () => import("@/views/HomeView.vue"),
meta: { // meta: {
title: "首页", // title: "首页",
requiresAuth: false, // requiresAuth: false,
menuType: 1, // NAVIGATION 类型,显示在顶部导航栏 // menuType: 1, // NAVIGATION 类型,显示在顶部导航栏
orderNum: -1, // 排在动态路由之前 // orderNum: -1, // 排在动态路由之前
}, // },
} // }
], // ],
}, // },
// 错误页面 // 错误页面
{ {
path: "/403", path: "/403",

View File

@@ -81,38 +81,46 @@ function generateRouteFromMenu(menu: SysMenu, isTopLevel = true): RouteRecordRaw
} }
}; };
// 如果有子菜单使用布局组件 // 检查是否指定了布局(只有顶层菜单使用布局
if (menu.children && menu.children.length > 0) { const layout = isTopLevel ? (menu as any).layout : null;
// 根据layout字段选择布局 const hasChildren = menu.children && menu.children.length > 0;
const layout = (menu as any).layout || menu.component;
if (layout) { // 确定路由组件
// 如果指定了layout使用指定的布局 if (layout && LAYOUT_MAP[layout]) {
route.component = getComponent(layout); // 如果指定了布局,使用指定的布局
route.component = getComponent(layout);
} else if (hasChildren && isTopLevel) {
// 如果有子菜单但没有指定布局,根据菜单类型选择默认布局
if (isTopLevel && menu.type === MenuType.NAVIGATION) {
route.component = getComponent('NavigationLayout');
} else if (menu.type === MenuType.SIDEBAR) {
route.component = getComponent('BlankLayout');
} else { } else {
// 根据菜单类型选择默认布局 route.component = getComponent('BasicLayout');
if (isTopLevel && menu.type === MenuType.NAVIGATION) {
route.component = getComponent('NavigationLayout');
} else if (menu.type === MenuType.SIDEBAR) {
route.component = getComponent('BlankLayout');
} else {
route.component = getComponent('BasicLayout');
}
} }
} else { } else {
// 没有子菜单,使用具体的页面组件 // 没有子菜单,也没有指定布局,使用具体的页面组件
if (menu.component) { if (menu.component) {
route.component = getComponent(menu.component); route.component = getComponent(menu.component);
} else { } else {
// 如果没有指定组件,使用BlankLayout作为默认 // 非顶层菜单没有组件,使用简单的占位组件
route.component = getComponent('BlankLayout'); route.component = getComponent('BlankLayout');
} }
} }
// 处理子菜单 // 处理子路由
if (menu.children && menu.children.length > 0) { if (layout && LAYOUT_MAP[layout] && !hasChildren && menu.component && isTopLevel) {
// 如果指定了布局但没有子菜单,将页面组件作为子路由
route.children = [{
path: '',
name: `${menu.menuID}_page`,
component: getComponent(menu.component),
meta: route.meta
}];
} else if (hasChildren) {
// 处理有子菜单的情况
route.children = []; route.children = [];
menu.children.forEach(child => { menu.children!.forEach(child => {
const childRoute = generateRouteFromMenu(child, false); const childRoute = generateRouteFromMenu(child, false);
if (childRoute) { if (childRoute) {
route.children!.push(childRoute); route.children!.push(childRoute);
@@ -121,7 +129,7 @@ function generateRouteFromMenu(menu: SysMenu, isTopLevel = true): RouteRecordRaw
// 如果没有设置重定向自动重定向到第一个有URL的子菜单 // 如果没有设置重定向自动重定向到第一个有URL的子菜单
if (!route.redirect && route.children.length > 0) { if (!route.redirect && route.children.length > 0) {
const firstChildWithUrl = findFirstMenuWithUrl(menu.children); const firstChildWithUrl = findFirstMenuWithUrl(menu.children!);
if (firstChildWithUrl?.url) { if (firstChildWithUrl?.url) {
route.redirect = firstChildWithUrl.url; route.redirect = firstChildWithUrl.url;
} }

View File

@@ -1,472 +0,0 @@
<template>
<div class="home-page">
<!-- 主横幅 -->
<section class="hero-section">
<div class="container">
<div class="hero-content">
<h1 class="hero-title">校园新闻管理系统</h1>
<p class="hero-subtitle">及时发布高效管理便捷浏览</p>
<div class="hero-actions">
<el-button type="primary" size="large" @click="exploreNews">
浏览新闻
</el-button>
<el-button size="large" @click="goToLogin" v-if="!isLoggedIn">
开始使用
</el-button>
</div>
</div>
</div>
</section>
<!-- 功能特性 -->
<section class="features-section">
<div class="container">
<h2 class="section-title">核心功能</h2>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon">📰</div>
<h3>新闻发布</h3>
<p>快速发布校园新闻支持富文本编辑图文并茂</p>
</div>
<div class="feature-card">
<div class="feature-icon">🔐</div>
<h3>权限管理</h3>
<p>细粒度权限控制安全可靠的用户管理体系</p>
</div>
<div class="feature-card">
<div class="feature-icon">📊</div>
<h3>数据统计</h3>
<p>实时统计新闻浏览量数据可视化展示</p>
</div>
<div class="feature-card">
<div class="feature-icon">💬</div>
<h3>评论互动</h3>
<p>支持新闻评论增强师生互动交流</p>
</div>
<div class="feature-card">
<div class="feature-icon">🔍</div>
<h3>智能搜索</h3>
<p>全文搜索快速定位所需新闻内容</p>
</div>
<div class="feature-card">
<div class="feature-icon">📱</div>
<h3>响应式设计</h3>
<p>完美适配各种设备随时随地浏览</p>
</div>
</div>
</div>
</section>
<!-- 最新新闻 -->
<section class="news-section">
<div class="container">
<h2 class="section-title">最新新闻</h2>
<div class="news-grid">
<div class="news-card" v-for="item in latestNews" :key="item.id">
<div class="news-image">
<img :src="item.image" :alt="item.title" />
</div>
<div class="news-content">
<span class="news-category">{{ item.category }}</span>
<h3 class="news-title">{{ item.title }}</h3>
<p class="news-excerpt">{{ item.excerpt }}</p>
<div class="news-meta">
<span class="news-date">{{ item.date }}</span>
<span class="news-views">👁 {{ item.views }}</span>
</div>
</div>
</div>
</div>
<div class="news-more">
<el-button @click="exploreNews">查看更多新闻</el-button>
</div>
</div>
</section>
<!-- 页脚 -->
<footer class="home-footer">
<div class="container">
<div class="footer-content">
<div class="footer-section">
<h4>关于我们</h4>
<p>校园新闻管理系统致力于为学校提供高效便捷的新闻发布和管理平台</p>
</div>
<div class="footer-section">
<h4>快速链接</h4>
<ul>
<li><a href="#home">首页</a></li>
<li><a href="#news">新闻中心</a></li>
<li><a href="#about">关于我们</a></li>
<li><a href="#contact">联系我们</a></li>
</ul>
</div>
<div class="footer-section">
<h4>联系方式</h4>
<ul>
<li>📧 Email: info@school-news.com</li>
<li>📞 电话: 123-456-7890</li>
<li>📍 地址: XX市XX区XX路XX号</li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>© 2025 校园新闻管理系统. All rights reserved.</p>
</div>
</div>
</footer>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { useStore } from 'vuex';
import { ElMessage } from 'element-plus';
const router = useRouter();
const store = useStore();
// 计算属性
const isLoggedIn = computed(() => store.getters['auth/isAuthenticated']);
const userName = computed(() => {
const userInfo = store.getters['auth/userInfo'];
return userInfo?.userName || userInfo?.nickName || '用户';
});
// 最新新闻数据(示例)
const latestNews = ref([
{
id: 1,
title: '我校在全国大学生创新创业大赛中获得金奖',
excerpt: '在刚刚结束的第十届全国大学生创新创业大赛中,我校代表队凭借优异的表现,荣获金奖...',
category: '校园动态',
image: 'https://picsum.photos/400/250?random=1',
date: '2025-10-08',
views: 1523
},
{
id: 2,
title: '校园文化艺术节圆满落幕',
excerpt: '历时一周的校园文化艺术节于昨日圆满落幕本次艺术节共举办了20余场精彩活动...',
category: '文化活动',
image: 'https://picsum.photos/400/250?random=2',
date: '2025-10-07',
views: 2341
},
{
id: 3,
title: '学校图书馆新增电子资源数据库',
excerpt: '为了更好地服务师生科研和学习,学校图书馆新增了多个电子资源数据库...',
category: '通知公告',
image: 'https://picsum.photos/400/250?random=3',
date: '2025-10-06',
views: 987
}
]);
// 方法
function goToLogin() {
router.push('/login');
}
function goToRegister() {
router.push('/register');
}
function goToDashboard() {
router.push('/dashboard');
}
async function handleLogout() {
try {
await store.dispatch('auth/logout');
ElMessage.success('已退出登录');
} catch (error) {
console.error('退出失败:', error);
ElMessage.error('退出失败');
}
}
function exploreNews() {
// TODO: 跳转到新闻列表页
ElMessage.info('新闻列表功能开发中...');
}
// 页面加载
onMounted(() => {
// 可以在这里加载真实的新闻数据
console.log('Home page mounted');
});
</script>
<style lang="scss" scoped>
.home-page {
min-height: 100vh;
background: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 24px;
}
/* 主横幅 */
.hero-section {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 100px 0;
text-align: center;
}
.hero-content {
max-width: 800px;
margin: 0 auto;
}
.hero-title {
font-size: 48px;
font-weight: 700;
margin: 0 0 20px 0;
}
.hero-subtitle {
font-size: 24px;
margin: 0 0 40px 0;
opacity: 0.9;
}
.hero-actions {
display: flex;
gap: 16px;
justify-content: center;
}
/* 功能特性 */
.features-section {
padding: 80px 0;
background: white;
}
.section-title {
text-align: center;
font-size: 36px;
font-weight: 600;
margin: 0 0 60px 0;
color: #333;
}
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 32px;
}
.feature-card {
text-align: center;
padding: 32px;
background: #f9f9f9;
border-radius: 8px;
transition: transform 0.3s, box-shadow 0.3s;
&:hover {
transform: translateY(-8px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}
.feature-icon {
font-size: 48px;
margin-bottom: 16px;
}
h3 {
font-size: 20px;
margin: 0 0 12px 0;
color: #333;
}
p {
color: #666;
line-height: 1.6;
margin: 0;
}
}
/* 最新新闻 */
.news-section {
padding: 80px 0;
background: #f5f5f5;
}
.news-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 32px;
margin-bottom: 40px;
}
.news-card {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.3s, box-shadow 0.3s;
cursor: pointer;
&:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
}
.news-image {
width: 100%;
height: 200px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s;
}
&:hover img {
transform: scale(1.1);
}
}
.news-content {
padding: 20px;
}
.news-category {
display: inline-block;
padding: 4px 12px;
background: #e6f7ff;
color: #1890ff;
border-radius: 4px;
font-size: 12px;
margin-bottom: 12px;
}
.news-title {
font-size: 18px;
font-weight: 600;
margin: 0 0 12px 0;
color: #333;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.news-excerpt {
color: #666;
font-size: 14px;
line-height: 1.6;
margin: 0 0 16px 0;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.news-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #999;
}
.news-more {
text-align: center;
}
/* 页脚 */
.home-footer {
background: #001529;
color: white;
padding: 60px 0 20px 0;
margin-top: auto;
}
.footer-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 40px;
margin-bottom: 40px;
}
.footer-section {
h4 {
font-size: 18px;
margin: 0 0 20px 0;
color: #1890ff;
}
p {
color: rgba(255, 255, 255, 0.8);
line-height: 1.6;
}
ul {
list-style: none;
padding: 0;
margin: 0;
li {
margin-bottom: 12px;
a {
color: rgba(255, 255, 255, 0.8);
text-decoration: none;
transition: color 0.3s;
&:hover {
color: #1890ff;
}
}
}
}
}
.footer-bottom {
text-align: center;
padding-top: 20px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.6);
}
/* 响应式设计 */
@media (max-width: 768px) {
.nav-menu {
display: none;
}
.hero-title {
font-size: 32px;
}
.hero-subtitle {
font-size: 18px;
}
.features-grid,
.news-grid {
grid-template-columns: 1fr;
}
.section-title {
font-size: 28px;
}
}
</style>