路由更新
This commit is contained in:
@@ -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
|
||||
('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()),
|
||||
('201', 'menu_party_history', '党史学习', 'menu_resource_center', '/resource-center/party-history', 'resource-center/PartyHistoryView', 'el-icon-trophy', 1, 1, 'BasicLayout', '1', now()),
|
||||
('202', 'menu_leader_speech', '领导讲话', 'menu_resource_center', '/resource-center/leader-speech', 'resource-center/LeaderSpeechView', 'el-icon-microphone', 2, 1, 'BasicLayout', '1', now()),
|
||||
('203', 'menu_policy_interpretation', '政策解读', 'menu_resource_center', '/resource-center/policy-interpretation', 'resource-center/PolicyInterpretationView', 'el-icon-document', 3, 1, 'BasicLayout', '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()),
|
||||
('205', 'menu_special_report', '专题报告', 'menu_resource_center', '/resource-center/special-report', 'resource-center/SpecialReportView', 'el-icon-document-copy', 5, 1, 'BasicLayout', '1', now()),
|
||||
('206', 'menu_world_case', '思政案例', 'menu_resource_center', '/resource-center/world-case', 'resource-center/WorldCaseView', 'el-icon-collection', 6, 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, 'NavigationLayout', '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, 'NavigationLayout', '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, 'NavigationLayout', '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()),
|
||||
('301', 'menu_study_tasks', '学习任务', 'menu_study_plan', '/study-plan/tasks', 'study-plan/StudyTasksView', 'el-icon-s-order', 1, 1, 'BasicLayout', '1', now()),
|
||||
('302', 'menu_course_center', '课程中心', 'menu_study_plan', '/study-plan/course', 'study-plan/CourseCenterView', 'el-icon-video-play', 2, 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, 'NavigationLayout', '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()),
|
||||
('401', 'menu_learning_records', '学习记录', 'menu_user_center', '/user-center/learning-records', 'user-center/LearningRecordsView', 'el-icon-document', 1, 1, 'BasicLayout', '1', now()),
|
||||
('402', 'menu_my_favorites', '我的收藏', 'menu_user_center', '/user-center/favorites', 'user-center/MyFavoritesView', 'el-icon-star-on', 2, 1, 'BasicLayout', '1', now()),
|
||||
('403', 'menu_my_achievements', '我的成就', 'menu_user_center', '/user-center/achievements', 'user-center/MyAchievementsView', 'el-icon-trophy', 3, 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, 'NavigationLayout', '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, 'NavigationLayout', '1', now()),
|
||||
|
||||
-- 账号中心
|
||||
('500', 'menu_profile', '账号中心', NULL, '/profile', 'profile/ProfileView', 'el-icon-user-solid', 5, 1, 'BasicLayout', '1', now()),
|
||||
('501', 'menu_personal_info', '个人信息', 'menu_profile', '/profile/personal-info', 'profile/PersonalInfoView', 'el-icon-user', 1, 1, 'BasicLayout', '1', now()),
|
||||
('502', 'menu_account_settings', '账号设置', 'menu_profile', '/profile/account-settings', 'profile/AccountSettingsView', 'el-icon-setting', 2, 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, 'NavigationLayout', '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
|
||||
|
||||
357
schoolNewsWeb/src/components/base/FloatingSidebar.vue
Normal file
357
schoolNewsWeb/src/components/base/FloatingSidebar.vue
Normal 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>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="nav-container">
|
||||
<!-- 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>
|
||||
</div>
|
||||
|
||||
@@ -23,13 +23,12 @@
|
||||
</div>
|
||||
|
||||
<!-- 下拉菜单 -->
|
||||
<Teleport to="body">
|
||||
<Teleport to="body" v-if="hasNavigationChildren(menu)">
|
||||
<div
|
||||
v-if="hasNavigationChildren(menu)"
|
||||
class="dropdown-menu"
|
||||
:class="{ show: activeDropdown === menu.menuID }"
|
||||
:style="getDropdownPosition(menu)"
|
||||
@mouseenter="handleMouseEnter(menu)"
|
||||
@mouseenter="() => { if (menu.menuID) activeDropdown = menu.menuID }"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<div
|
||||
@@ -58,7 +57,7 @@
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
<div class="search-icon">
|
||||
<img src="@/assets/imgs/search-icon.svg" alt="搜索" />
|
||||
<img src="../../assets/imgs/search-icon.svg" alt="搜索" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -152,9 +151,11 @@ function handleMouseEnter(menu: SysMenu, event?: MouseEvent) {
|
||||
activeDropdown.value = menu.menuID || null;
|
||||
|
||||
// 计算下拉菜单位置
|
||||
const target = event?.currentTarget as HTMLElement;
|
||||
if (target && menu.menuID) {
|
||||
const rect = target.getBoundingClientRect();
|
||||
if (event && menu.menuID) {
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
const navLink = target.querySelector('.nav-link') as HTMLElement;
|
||||
const rect = (navLink || target).getBoundingClientRect();
|
||||
|
||||
dropdownPositions.value[menu.menuID] = {
|
||||
left: rect.left,
|
||||
top: rect.bottom,
|
||||
@@ -167,11 +168,15 @@ function handleMouseEnter(menu: SysMenu, event?: MouseEvent) {
|
||||
// 获取下拉菜单位置样式
|
||||
function getDropdownPosition(menu: SysMenu) {
|
||||
const menuID = menu.menuID;
|
||||
if (!menuID || !dropdownPositions.value[menuID]) {
|
||||
return {};
|
||||
const pos = menuID && dropdownPositions.value[menuID];
|
||||
|
||||
if (!pos) {
|
||||
return {
|
||||
position: 'fixed' as const,
|
||||
visibility: 'hidden' as const
|
||||
};
|
||||
}
|
||||
|
||||
const pos = dropdownPositions.value[menuID];
|
||||
return {
|
||||
position: 'fixed' as const,
|
||||
left: `${pos.left}px`,
|
||||
@@ -356,15 +361,16 @@ function handleLogout() {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-10px);
|
||||
transition: all 0.3s;
|
||||
transform: scaleY(0);
|
||||
transform-origin: top center;
|
||||
transition: opacity 0.2s, transform 0.2s, visibility 0.2s;
|
||||
z-index: 10000;
|
||||
pointer-events: none;
|
||||
|
||||
&.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
transform: scaleY(1);
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
6
schoolNewsWeb/src/components/base/index.ts
Normal file
6
schoolNewsWeb/src/components/base/index.ts
Normal 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';
|
||||
3
schoolNewsWeb/src/components/index.ts
Normal file
3
schoolNewsWeb/src/components/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// 导出 base 基础组件
|
||||
export * from './base';
|
||||
|
||||
@@ -57,12 +57,7 @@ import { useRoute, useRouter } from "vue-router";
|
||||
import { useStore } from "vuex";
|
||||
import type { SysMenu } from "@/types";
|
||||
import { getMenuPath } from "@/utils/route-generator";
|
||||
// @ts-ignore - Vue 3.5 defineOptions支持
|
||||
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";
|
||||
import { MenuNav, Breadcrumb, UserDropdown } from "@/components/base";
|
||||
|
||||
// 响应式状态
|
||||
const sidebarCollapsed = ref(false);
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
<!-- 主内容区域 -->
|
||||
<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" />
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- 侧边栏和内容 -->
|
||||
<div class="content-wrapper" v-if="hasSidebarMenus">
|
||||
@@ -50,12 +50,7 @@ import { useStore } from 'vuex';
|
||||
import type { SysMenu } from '@/types';
|
||||
import { MenuType } from '@/types/enums';
|
||||
import { getMenuPath } from '@/utils/route-generator';
|
||||
// @ts-ignore - Vue 3.5 组件导入兼容性
|
||||
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';
|
||||
import { TopNavigation, MenuNav, Breadcrumb } from '@/components';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
@@ -57,23 +57,23 @@ export const routes: Array<RouteRecordRaw> = [
|
||||
],
|
||||
},
|
||||
// 首页(显示在导航栏)
|
||||
{
|
||||
path: "/home",
|
||||
component: () => import("@/layouts/NavigationLayout.vue"),
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "Home",
|
||||
component: () => import("@/views/HomeView.vue"),
|
||||
meta: {
|
||||
title: "首页",
|
||||
requiresAuth: false,
|
||||
menuType: 1, // NAVIGATION 类型,显示在顶部导航栏
|
||||
orderNum: -1, // 排在动态路由之前
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
// {
|
||||
// path: "/home",
|
||||
// component: () => import("@/layouts/NavigationLayout.vue"),
|
||||
// children: [
|
||||
// {
|
||||
// path: "",
|
||||
// name: "Home",
|
||||
// component: () => import("@/views/HomeView.vue"),
|
||||
// meta: {
|
||||
// title: "首页",
|
||||
// requiresAuth: false,
|
||||
// menuType: 1, // NAVIGATION 类型,显示在顶部导航栏
|
||||
// orderNum: -1, // 排在动态路由之前
|
||||
// },
|
||||
// }
|
||||
// ],
|
||||
// },
|
||||
// 错误页面
|
||||
{
|
||||
path: "/403",
|
||||
|
||||
@@ -81,38 +81,46 @@ function generateRouteFromMenu(menu: SysMenu, isTopLevel = true): RouteRecordRaw
|
||||
}
|
||||
};
|
||||
|
||||
// 如果有子菜单,使用布局组件
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
// 根据layout字段选择布局
|
||||
const layout = (menu as any).layout || menu.component;
|
||||
|
||||
if (layout) {
|
||||
// 如果指定了layout,使用指定的布局
|
||||
route.component = getComponent(layout);
|
||||
// 检查是否指定了布局(只有顶层菜单才使用布局)
|
||||
const layout = isTopLevel ? (menu as any).layout : null;
|
||||
const hasChildren = menu.children && menu.children.length > 0;
|
||||
|
||||
// 确定路由组件
|
||||
if (layout && LAYOUT_MAP[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 {
|
||||
// 根据菜单类型选择默认布局
|
||||
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');
|
||||
}
|
||||
route.component = getComponent('BasicLayout');
|
||||
}
|
||||
} else {
|
||||
// 没有子菜单,使用具体的页面组件
|
||||
// 没有子菜单,也没有指定布局,使用具体的页面组件
|
||||
if (menu.component) {
|
||||
route.component = getComponent(menu.component);
|
||||
} else {
|
||||
// 如果没有指定组件,使用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 = [];
|
||||
menu.children.forEach(child => {
|
||||
menu.children!.forEach(child => {
|
||||
const childRoute = generateRouteFromMenu(child, false);
|
||||
if (childRoute) {
|
||||
route.children!.push(childRoute);
|
||||
@@ -121,7 +129,7 @@ function generateRouteFromMenu(menu: SysMenu, isTopLevel = true): RouteRecordRaw
|
||||
|
||||
// 如果没有设置重定向,自动重定向到第一个有URL的子菜单
|
||||
if (!route.redirect && route.children.length > 0) {
|
||||
const firstChildWithUrl = findFirstMenuWithUrl(menu.children);
|
||||
const firstChildWithUrl = findFirstMenuWithUrl(menu.children!);
|
||||
if (firstChildWithUrl?.url) {
|
||||
route.redirect = firstChildWithUrl.url;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user