diff --git a/src/api/modules/menu.ts b/src/api/modules/menu.ts new file mode 100644 index 0000000..c2b8690 --- /dev/null +++ b/src/api/modules/menu.ts @@ -0,0 +1,35 @@ +// 菜单相关API +import { ApiRequest } from '../request' +import type { ApiResponse } from '../types' + +// 菜单项接口定义 +export interface MenuItem { + id: string + name: string + path: string + type: string + icon: string | null + parentId: string | null + sortOrder: number + izVisible: number + permissionKey: string | null +} + +// 菜单API类 +export class MenuApi { + /** + * 获取首页菜单 + */ + static async getIndexMenus(): Promise> { + return await ApiRequest.get('/aiol/aiolMenu/getIndexMenus') + } + + /** + * 获取学生菜单 + */ + static async getStudentMenus(): Promise> { + return await ApiRequest.get('/aiol/aiolMenu/getStudentMenus') + } +} + +export default MenuApi diff --git a/src/components/layout/DynamicNavigation.vue b/src/components/layout/DynamicNavigation.vue new file mode 100644 index 0000000..1ca42b0 --- /dev/null +++ b/src/components/layout/DynamicNavigation.vue @@ -0,0 +1,186 @@ + + + + + diff --git a/src/stores/menu.ts b/src/stores/menu.ts new file mode 100644 index 0000000..5d0300d --- /dev/null +++ b/src/stores/menu.ts @@ -0,0 +1,106 @@ +// 菜单状态管理 +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import type { MenuItem } from '@/api/modules/menu' +import { MenuApi } from '@/api/modules/menu' +import { generateNavMenus } from '@/utils/routeUtils' + +export const useMenuStore = defineStore('menu', () => { + // 状态 + const indexMenus = ref([]) + const studentMenus = ref([]) + const loading = ref(false) + const error = ref(null) + + // 计算属性 + const navMenus = computed(() => generateNavMenus(indexMenus.value)) + + const visibleIndexMenus = computed(() => + indexMenus.value + .filter(menu => menu.izVisible === 1) + .sort((a, b) => b.sortOrder - a.sortOrder) + ) + + const visibleStudentMenus = computed(() => + studentMenus.value + .filter(menu => menu.izVisible === 1) + .sort((a, b) => b.sortOrder - a.sortOrder) + ) + + // 方法 + const fetchIndexMenus = async () => { + try { + loading.value = true + error.value = null + + const response = await MenuApi.getIndexMenus() + if (response.code === 200) { + indexMenus.value = response.data || [] + console.log('✅ 首页菜单加载成功:', indexMenus.value) + } else { + throw new Error(response.message || '获取首页菜单失败') + } + } catch (err: any) { + error.value = err.message || '获取首页菜单失败' + console.error('❌ 获取首页菜单失败:', err) + } finally { + loading.value = false + } + } + + const fetchStudentMenus = async () => { + try { + loading.value = true + error.value = null + + const response = await MenuApi.getStudentMenus() + if (response.code === 200) { + studentMenus.value = response.data || [] + console.log('✅ 学生菜单加载成功:', studentMenus.value) + } else { + throw new Error(response.message || '获取学生菜单失败') + } + } catch (err: any) { + error.value = err.message || '获取学生菜单失败' + console.error('❌ 获取学生菜单失败:', err) + } finally { + loading.value = false + } + } + + const getMenuByPath = (path: string, type: 'index' | 'student' = 'index'): MenuItem | undefined => { + const menus = type === 'index' ? indexMenus.value : studentMenus.value + return menus.find(menu => menu.path === path) + } + + const isRouteVisible = (path: string, type: 'index' | 'student' = 'index'): boolean => { + const menu = getMenuByPath(path, type) + return menu ? menu.izVisible === 1 : false + } + + const clearMenus = () => { + indexMenus.value = [] + studentMenus.value = [] + error.value = null + } + + return { + // 状态 + indexMenus, + studentMenus, + loading, + error, + + // 计算属性 + navMenus, + visibleIndexMenus, + visibleStudentMenus, + + // 方法 + fetchIndexMenus, + fetchStudentMenus, + getMenuByPath, + isRouteVisible, + clearMenus, + } +}) diff --git a/src/utils/routeUtils.ts b/src/utils/routeUtils.ts new file mode 100644 index 0000000..22479cb --- /dev/null +++ b/src/utils/routeUtils.ts @@ -0,0 +1,114 @@ +// 路由工具函数 +import type { RouteRecordRaw } from 'vue-router' +import type { MenuItem } from '@/api/modules/menu' + +// 组件映射表 - 将路径映射到对应的组件 +const componentMap: Record Promise> = { + '/': () => import('@/views/Home.vue'), + '/courses': () => import('@/views/Courses.vue'), + '/special-training': () => import('@/views/SpecialTraining.vue'), + '/faculty': () => import('@/views/Faculty.vue'), + '/resources': () => import('@/views/Resources.vue'), + '/activities': () => import('@/views/Activities.vue'), + '/ai/app': () => import('@/views/Ai/AiAppList-NaiveUI.vue'), +} + +// 路由名称映射表 - 将路径映射到路由名称 +const routeNameMap: Record = { + '/': 'Home', + '/courses': 'Courses', + '/special-training': 'SpecialTraining', + '/faculty': 'Faculty', + '/resources': 'Resources', + '/activities': 'Activities', + '/ai/app': 'AiAppList', +} + +// 路由标题映射表 - 将路径映射到页面标题 +const routeTitleMap: Record = { + '/': '首页', + '/courses': '课程列表', + '/special-training': '专题训练', + '/faculty': '师资力量', + '/resources': '精选资源', + '/activities': '全部活动', + '/ai/app': 'AI应用管理', +} + +/** + * 根据菜单数据生成路由配置 + * @param menus 菜单数据数组 + * @returns 路由配置数组 + */ +export function generateRoutesFromMenus(menus: MenuItem[]): RouteRecordRaw[] { + const routes: RouteRecordRaw[] = [] + + // 过滤可见的菜单项并按排序字段排序 + const visibleMenus = menus + .filter(menu => menu.izVisible === 1) + .sort((a, b) => b.sortOrder - a.sortOrder) // 降序排列 + + for (const menu of visibleMenus) { + const { path, name } = menu + + // 检查路径是否有对应的组件 + if (!componentMap[path]) { + console.warn(`路径 ${path} 没有对应的组件映射,跳过生成路由`) + continue + } + + const route: RouteRecordRaw = { + path, + name: routeNameMap[path] || name, + component: componentMap[path], + meta: { + title: routeTitleMap[path] || name, + menuId: menu.id, + sortOrder: menu.sortOrder, + permissionKey: menu.permissionKey, + } + } + + routes.push(route) + } + + return routes +} + +/** + * 生成导航菜单数据 + * @param menus 菜单数据数组 + * @returns 导航菜单数组 + */ +export function generateNavMenus(menus: MenuItem[]) { + return menus + .filter(menu => menu.izVisible === 1) + .sort((a, b) => b.sortOrder - a.sortOrder) + .map(menu => ({ + id: menu.id, + name: menu.name, + path: menu.path, + icon: menu.icon, + sortOrder: menu.sortOrder, + })) +} + +/** + * 检查路由是否存在于菜单中 + * @param path 路由路径 + * @param menus 菜单数据数组 + * @returns 是否存在 + */ +export function isRouteInMenus(path: string, menus: MenuItem[]): boolean { + return menus.some(menu => menu.path === path && menu.izVisible === 1) +} + +/** + * 根据路径获取菜单项 + * @param path 路由路径 + * @param menus 菜单数据数组 + * @returns 菜单项或undefined + */ +export function getMenuByPath(path: string, menus: MenuItem[]): MenuItem | undefined { + return menus.find(menu => menu.path === path) +} diff --git a/src/views/AiModelTest.vue b/src/views/AiModelTest.vue new file mode 100644 index 0000000..0c5d40a --- /dev/null +++ b/src/views/AiModelTest.vue @@ -0,0 +1,320 @@ + + + + + diff --git a/src/views/MenuTest.vue b/src/views/MenuTest.vue new file mode 100644 index 0000000..21d8f0d --- /dev/null +++ b/src/views/MenuTest.vue @@ -0,0 +1,218 @@ + + + + +