Merge branch 'dev' of http://110.42.96.64:19890/GoCo/OL-LearnPlatform-Frontend into dev
This commit is contained in:
commit
8d2b4037d4
35
src/api/modules/menu.ts
Normal file
35
src/api/modules/menu.ts
Normal file
@ -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<ApiResponse<MenuItem[]>> {
|
||||||
|
return await ApiRequest.get('/aiol/aiolMenu/getIndexMenus')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取学生菜单
|
||||||
|
*/
|
||||||
|
static async getStudentMenus(): Promise<ApiResponse<MenuItem[]>> {
|
||||||
|
return await ApiRequest.get('/aiol/aiolMenu/getStudentMenus')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MenuApi
|
186
src/components/layout/DynamicNavigation.vue
Normal file
186
src/components/layout/DynamicNavigation.vue
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dynamic-navigation">
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<div v-if="menuStore.loading" class="loading">
|
||||||
|
<n-spin size="small" />
|
||||||
|
<span>加载菜单中...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 错误状态 -->
|
||||||
|
<div v-else-if="menuStore.error" class="error">
|
||||||
|
<n-alert type="error" :title="menuStore.error" />
|
||||||
|
<n-button @click="handleRetry" size="small" type="primary">
|
||||||
|
重试
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 菜单列表 -->
|
||||||
|
<nav v-else class="nav-menu">
|
||||||
|
<router-link
|
||||||
|
v-for="menu in menuStore.navMenus"
|
||||||
|
:key="menu.id"
|
||||||
|
:to="menu.path"
|
||||||
|
class="nav-item"
|
||||||
|
:class="{ active: isCurrentRoute(menu.path) }"
|
||||||
|
>
|
||||||
|
<n-icon v-if="menu.icon" :component="getIcon(menu.icon)" />
|
||||||
|
<span>{{ menu.name }}</span>
|
||||||
|
</router-link>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- 调试信息(开发环境) -->
|
||||||
|
<div v-if="isDev" class="debug-info">
|
||||||
|
<n-collapse>
|
||||||
|
<n-collapse-item title="菜单调试信息" name="debug">
|
||||||
|
<pre>{{ JSON.stringify(menuStore.indexMenus, null, 2) }}</pre>
|
||||||
|
</n-collapse-item>
|
||||||
|
</n-collapse>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import {
|
||||||
|
NSpin,
|
||||||
|
NAlert,
|
||||||
|
NButton,
|
||||||
|
NIcon,
|
||||||
|
NCollapse,
|
||||||
|
NCollapseItem
|
||||||
|
} from 'naive-ui'
|
||||||
|
import {
|
||||||
|
HomeOutlined,
|
||||||
|
BookOutlined,
|
||||||
|
TeamOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
CalendarOutlined,
|
||||||
|
RobotOutlined
|
||||||
|
} from '@vicons/antd'
|
||||||
|
import { useMenuStore } from '@/stores/menu'
|
||||||
|
|
||||||
|
// 图标映射
|
||||||
|
const iconMap: Record<string, any> = {
|
||||||
|
home: HomeOutlined,
|
||||||
|
book: BookOutlined,
|
||||||
|
team: TeamOutlined,
|
||||||
|
resource: FileTextOutlined,
|
||||||
|
activity: CalendarOutlined,
|
||||||
|
ai: RobotOutlined,
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuStore = useMenuStore()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
// 是否为开发环境
|
||||||
|
const isDev = computed(() => import.meta.env.DEV)
|
||||||
|
|
||||||
|
// 获取图标组件
|
||||||
|
const getIcon = (iconName: string) => {
|
||||||
|
return iconMap[iconName] || HomeOutlined
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为当前路由
|
||||||
|
const isCurrentRoute = (path: string) => {
|
||||||
|
return route.path === path
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重试加载菜单
|
||||||
|
const handleRetry = async () => {
|
||||||
|
await menuStore.fetchIndexMenus()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载时加载菜单
|
||||||
|
onMounted(async () => {
|
||||||
|
if (menuStore.indexMenus.length === 0) {
|
||||||
|
await menuStore.fetchIndexMenus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dynamic-navigation {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #333;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
background-color: #e6f7ff;
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-info {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-info pre {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.nav-menu {
|
||||||
|
flex-direction: row;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 80px;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item span {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
106
src/stores/menu.ts
Normal file
106
src/stores/menu.ts
Normal file
@ -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<MenuItem[]>([])
|
||||||
|
const studentMenus = ref<MenuItem[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref<string | null>(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,
|
||||||
|
}
|
||||||
|
})
|
114
src/utils/routeUtils.ts
Normal file
114
src/utils/routeUtils.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
// 路由工具函数
|
||||||
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
|
import type { MenuItem } from '@/api/modules/menu'
|
||||||
|
|
||||||
|
// 组件映射表 - 将路径映射到对应的组件
|
||||||
|
const componentMap: Record<string, () => Promise<any>> = {
|
||||||
|
'/': () => 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<string, string> = {
|
||||||
|
'/': 'Home',
|
||||||
|
'/courses': 'Courses',
|
||||||
|
'/special-training': 'SpecialTraining',
|
||||||
|
'/faculty': 'Faculty',
|
||||||
|
'/resources': 'Resources',
|
||||||
|
'/activities': 'Activities',
|
||||||
|
'/ai/app': 'AiAppList',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 路由标题映射表 - 将路径映射到页面标题
|
||||||
|
const routeTitleMap: Record<string, string> = {
|
||||||
|
'/': '首页',
|
||||||
|
'/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)
|
||||||
|
}
|
320
src/views/AiModelTest.vue
Normal file
320
src/views/AiModelTest.vue
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
<template>
|
||||||
|
<div class="ai-model-test">
|
||||||
|
<n-card title="AI模型字典API测试">
|
||||||
|
<n-space vertical size="large">
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<n-card title="操作" size="small">
|
||||||
|
<n-space>
|
||||||
|
<n-button
|
||||||
|
@click="handleLoadModels"
|
||||||
|
:loading="loading"
|
||||||
|
type="primary"
|
||||||
|
>
|
||||||
|
加载AI模型列表
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
@click="handleDirectRequest"
|
||||||
|
:loading="loading"
|
||||||
|
type="info"
|
||||||
|
>
|
||||||
|
直接请求测试
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="handleClearModels">
|
||||||
|
清空数据
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 状态信息 -->
|
||||||
|
<n-card title="状态信息" size="small">
|
||||||
|
<n-descriptions :column="2" bordered>
|
||||||
|
<n-descriptions-item label="加载状态">
|
||||||
|
<n-tag :type="loading ? 'warning' : 'success'">
|
||||||
|
{{ loading ? '加载中' : '已完成' }}
|
||||||
|
</n-tag>
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="错误信息">
|
||||||
|
<n-tag v-if="error" type="error">
|
||||||
|
{{ error }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag v-else type="success">无错误</n-tag>
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="模型数量">
|
||||||
|
{{ modelList.length }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="API地址">
|
||||||
|
<n-code>/sys/dict/getDictItems/airag_model where model_type = 'LLM',name,id</n-code>
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="用户Token">
|
||||||
|
<n-tag v-if="userStore.token" type="success">已登录</n-tag>
|
||||||
|
<n-tag v-else type="warning">未登录</n-tag>
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="Base URL">
|
||||||
|
<n-code>{{ baseURL }}</n-code>
|
||||||
|
</n-descriptions-item>
|
||||||
|
</n-descriptions>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 模型列表 -->
|
||||||
|
<n-card title="AI模型列表" size="small">
|
||||||
|
<n-data-table
|
||||||
|
:columns="modelColumns"
|
||||||
|
:data="modelList"
|
||||||
|
:pagination="false"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 选择器测试 -->
|
||||||
|
<n-card title="选择器测试" size="small">
|
||||||
|
<n-form-item label="选择AI模型">
|
||||||
|
<n-select
|
||||||
|
v-model:value="selectedModel"
|
||||||
|
placeholder="请选择AI模型"
|
||||||
|
:options="modelOptions"
|
||||||
|
:loading="loading"
|
||||||
|
@update:value="handleModelSelect"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="当前选择">
|
||||||
|
<n-tag v-if="selectedModel" type="info">
|
||||||
|
{{ getSelectedModelText() }}
|
||||||
|
</n-tag>
|
||||||
|
<span v-else>未选择</span>
|
||||||
|
</n-form-item>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 原始数据 -->
|
||||||
|
<n-card title="原始API响应数据" size="small">
|
||||||
|
<n-code
|
||||||
|
:code="JSON.stringify(rawData, null, 2)"
|
||||||
|
language="json"
|
||||||
|
show-line-numbers
|
||||||
|
/>
|
||||||
|
</n-card>
|
||||||
|
</n-space>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, h } from 'vue'
|
||||||
|
import {
|
||||||
|
NCard,
|
||||||
|
NSpace,
|
||||||
|
NDescriptions,
|
||||||
|
NDescriptionsItem,
|
||||||
|
NTag,
|
||||||
|
NButton,
|
||||||
|
NDataTable,
|
||||||
|
NCode,
|
||||||
|
NFormItem,
|
||||||
|
NSelect,
|
||||||
|
useMessage
|
||||||
|
} from 'naive-ui'
|
||||||
|
import type { DataTableColumns } from 'naive-ui'
|
||||||
|
import { SystemApi, type DictItem } from '@/api'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// 状态
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref<string | null>(null)
|
||||||
|
const modelList = ref<DictItem[]>([])
|
||||||
|
const rawData = ref<any>(null)
|
||||||
|
const selectedModel = ref<string>('')
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const baseURL = computed(() => import.meta.env.VITE_API_BASE_URL || '/jeecgboot')
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const modelOptions = computed(() =>
|
||||||
|
modelList.value.map(item => ({
|
||||||
|
label: item.text,
|
||||||
|
value: item.value
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
// 表格列定义
|
||||||
|
const modelColumns: DataTableColumns<DictItem> = [
|
||||||
|
{
|
||||||
|
title: 'Value',
|
||||||
|
key: 'value',
|
||||||
|
width: 200,
|
||||||
|
render: (row) => {
|
||||||
|
return h('code', {
|
||||||
|
style: 'background: #f5f5f5; padding: 2px 4px; border-radius: 3px;'
|
||||||
|
}, row.value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Text',
|
||||||
|
key: 'text',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Label',
|
||||||
|
key: 'label',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Title',
|
||||||
|
key: 'title',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Color',
|
||||||
|
key: 'color',
|
||||||
|
width: 100,
|
||||||
|
render: (row) => {
|
||||||
|
return row.color ? h(NTag, { type: 'info' }, () => row.color) : '无'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 加载AI模型列表
|
||||||
|
const handleLoadModels = async () => {
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('🚀 开始加载AI模型列表...')
|
||||||
|
console.log('🔍 当前时间戳:', Date.now())
|
||||||
|
console.log('🔍 请求URL:', '/sys/dict/getDictItems/airag_model%20where%20model_type%20=%20\'LLM\',name,id')
|
||||||
|
|
||||||
|
const response = await SystemApi.getAiModelDict()
|
||||||
|
|
||||||
|
console.log('📦 完整API响应:', response)
|
||||||
|
console.log('📦 响应数据:', response.data)
|
||||||
|
|
||||||
|
rawData.value = response
|
||||||
|
|
||||||
|
if (response.code === 200 || response.code === 0) {
|
||||||
|
modelList.value = response.data || []
|
||||||
|
message.success(`加载成功,共${modelList.value.length}个模型`)
|
||||||
|
console.log('✅ AI模型列表加载成功:', modelList.value)
|
||||||
|
} else {
|
||||||
|
throw new Error(response.message || '获取模型列表失败')
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value = err.message || '加载失败'
|
||||||
|
message.error(error.value || '未知错误')
|
||||||
|
console.error('❌ 加载AI模型列表失败:', err)
|
||||||
|
console.error('❌ 错误详情:', {
|
||||||
|
message: err.message,
|
||||||
|
response: err.response,
|
||||||
|
config: err.config,
|
||||||
|
stack: err.stack
|
||||||
|
})
|
||||||
|
|
||||||
|
// 保存错误信息到rawData以便查看
|
||||||
|
rawData.value = {
|
||||||
|
error: err.message,
|
||||||
|
response: err.response?.data,
|
||||||
|
config: err.config
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接请求测试
|
||||||
|
const handleDirectRequest = async () => {
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('🚀 开始直接请求测试...')
|
||||||
|
|
||||||
|
const baseURL = import.meta.env.VITE_API_BASE_URL || '/jeecgboot'
|
||||||
|
const timestamp = Date.now()
|
||||||
|
const token = userStore.token || localStorage.getItem('X-Access-Token') || ''
|
||||||
|
|
||||||
|
const url = `${baseURL}/sys/dict/getDictItems/airag_model%20where%20model_type%20=%20'LLM',name,id?_t=${timestamp}`
|
||||||
|
|
||||||
|
console.log('🔍 请求信息:', {
|
||||||
|
url,
|
||||||
|
timestamp,
|
||||||
|
token: token ? '***' : '无',
|
||||||
|
baseURL
|
||||||
|
})
|
||||||
|
|
||||||
|
const headers: any = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Request-Time': timestamp.toString(),
|
||||||
|
'timestamp': timestamp.toString(),
|
||||||
|
'X-Timestamp': timestamp.toString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
headers['X-Access-Token'] = token
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔍 请求头:', headers)
|
||||||
|
|
||||||
|
const response = await axios.get(url, { headers })
|
||||||
|
|
||||||
|
console.log('📦 直接请求响应:', response)
|
||||||
|
rawData.value = response.data
|
||||||
|
|
||||||
|
if (response.data.code === 200 || response.data.code === 0) {
|
||||||
|
modelList.value = response.data.result || response.data.data || []
|
||||||
|
message.success(`直接请求成功,共${modelList.value.length}个模型`)
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.message || '直接请求失败')
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value = err.message || '直接请求失败'
|
||||||
|
message.error(error.value || '未知错误')
|
||||||
|
console.error('❌ 直接请求失败:', err)
|
||||||
|
|
||||||
|
rawData.value = {
|
||||||
|
error: err.message,
|
||||||
|
response: err.response?.data,
|
||||||
|
status: err.response?.status,
|
||||||
|
headers: err.response?.headers
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空数据
|
||||||
|
const handleClearModels = () => {
|
||||||
|
modelList.value = []
|
||||||
|
rawData.value = null
|
||||||
|
selectedModel.value = ''
|
||||||
|
error.value = null
|
||||||
|
message.info('数据已清空')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模型选择
|
||||||
|
const handleModelSelect = (value: string) => {
|
||||||
|
const model = modelList.value.find(item => item.value === value)
|
||||||
|
if (model) {
|
||||||
|
message.info(`已选择: ${model.text}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取选择的模型文本
|
||||||
|
const getSelectedModelText = () => {
|
||||||
|
const model = modelList.value.find(item => item.value === selectedModel.value)
|
||||||
|
return model ? model.text : selectedModel.value
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ai-model-test {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-code) {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
218
src/views/MenuTest.vue
Normal file
218
src/views/MenuTest.vue
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
<template>
|
||||||
|
<div class="menu-test">
|
||||||
|
<n-card title="动态菜单路由测试">
|
||||||
|
<n-space vertical size="large">
|
||||||
|
<!-- 菜单状态 -->
|
||||||
|
<n-card title="菜单状态" size="small">
|
||||||
|
<n-descriptions :column="2" bordered>
|
||||||
|
<n-descriptions-item label="加载状态">
|
||||||
|
<n-tag :type="menuStore.loading ? 'warning' : 'success'">
|
||||||
|
{{ menuStore.loading ? '加载中' : '已加载' }}
|
||||||
|
</n-tag>
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="错误信息">
|
||||||
|
<n-tag v-if="menuStore.error" type="error">
|
||||||
|
{{ menuStore.error }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag v-else type="success">无错误</n-tag>
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="首页菜单数量">
|
||||||
|
{{ menuStore.indexMenus.length }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="可见菜单数量">
|
||||||
|
{{ menuStore.visibleIndexMenus.length }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
</n-descriptions>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<n-card title="操作" size="small">
|
||||||
|
<n-space>
|
||||||
|
<n-button
|
||||||
|
@click="handleRefreshMenus"
|
||||||
|
:loading="menuStore.loading"
|
||||||
|
type="primary"
|
||||||
|
>
|
||||||
|
刷新菜单
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="handleClearMenus">
|
||||||
|
清空菜单
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="handleTestRoute">
|
||||||
|
测试路由跳转
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 菜单列表 -->
|
||||||
|
<n-card title="菜单列表" size="small">
|
||||||
|
<n-data-table
|
||||||
|
:columns="menuColumns"
|
||||||
|
:data="menuStore.visibleIndexMenus"
|
||||||
|
:pagination="false"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 路由信息 -->
|
||||||
|
<n-card title="当前路由信息" size="small">
|
||||||
|
<n-descriptions :column="1" bordered>
|
||||||
|
<n-descriptions-item label="路径">
|
||||||
|
{{ $route.path }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="名称">
|
||||||
|
{{ $route.name }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="标题">
|
||||||
|
{{ $route.meta.title }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="菜单ID">
|
||||||
|
{{ $route.meta.menuId || '无' }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
</n-descriptions>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 动态导航组件 -->
|
||||||
|
<n-card title="动态导航组件" size="small">
|
||||||
|
<DynamicNavigation />
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 原始数据 -->
|
||||||
|
<n-card title="原始菜单数据" size="small">
|
||||||
|
<n-code
|
||||||
|
:code="JSON.stringify(menuStore.indexMenus, null, 2)"
|
||||||
|
language="json"
|
||||||
|
show-line-numbers
|
||||||
|
/>
|
||||||
|
</n-card>
|
||||||
|
</n-space>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { h } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import {
|
||||||
|
NCard,
|
||||||
|
NSpace,
|
||||||
|
NDescriptions,
|
||||||
|
NDescriptionsItem,
|
||||||
|
NTag,
|
||||||
|
NButton,
|
||||||
|
NDataTable,
|
||||||
|
NCode,
|
||||||
|
useMessage
|
||||||
|
} from 'naive-ui'
|
||||||
|
import type { DataTableColumns } from 'naive-ui'
|
||||||
|
import { useMenuStore } from '@/stores/menu'
|
||||||
|
import DynamicNavigation from '@/components/layout/DynamicNavigation.vue'
|
||||||
|
import type { MenuItem } from '@/api/modules/menu'
|
||||||
|
|
||||||
|
const menuStore = useMenuStore()
|
||||||
|
const router = useRouter()
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
// 表格列定义
|
||||||
|
const menuColumns: DataTableColumns<MenuItem> = [
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
key: 'id',
|
||||||
|
width: 60,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '名称',
|
||||||
|
key: 'name',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '路径',
|
||||||
|
key: 'path',
|
||||||
|
width: 150,
|
||||||
|
render: (row) => {
|
||||||
|
return h('code', { style: 'background: #f5f5f5; padding: 2px 4px; border-radius: 3px;' }, row.path)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '类型',
|
||||||
|
key: 'type',
|
||||||
|
width: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '排序',
|
||||||
|
key: 'sortOrder',
|
||||||
|
width: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '可见',
|
||||||
|
key: 'izVisible',
|
||||||
|
width: 80,
|
||||||
|
render: (row) => {
|
||||||
|
return h(NTag, {
|
||||||
|
type: row.izVisible === 1 ? 'success' : 'error'
|
||||||
|
}, () => row.izVisible === 1 ? '是' : '否')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'actions',
|
||||||
|
width: 100,
|
||||||
|
render: (row) => {
|
||||||
|
return h(NButton, {
|
||||||
|
size: 'small',
|
||||||
|
type: 'primary',
|
||||||
|
onClick: () => handleNavigate(row.path)
|
||||||
|
}, () => '跳转')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 刷新菜单
|
||||||
|
const handleRefreshMenus = async () => {
|
||||||
|
try {
|
||||||
|
await menuStore.fetchIndexMenus()
|
||||||
|
message.success('菜单刷新成功')
|
||||||
|
} catch (error) {
|
||||||
|
message.error('菜单刷新失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空菜单
|
||||||
|
const handleClearMenus = () => {
|
||||||
|
menuStore.clearMenus()
|
||||||
|
message.info('菜单已清空')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试路由跳转
|
||||||
|
const handleTestRoute = () => {
|
||||||
|
const menus = menuStore.visibleIndexMenus
|
||||||
|
if (menus.length > 0) {
|
||||||
|
const randomMenu = menus[Math.floor(Math.random() * menus.length)]
|
||||||
|
handleNavigate(randomMenu.path)
|
||||||
|
} else {
|
||||||
|
message.warning('没有可用的菜单项')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导航到指定路径
|
||||||
|
const handleNavigate = (path: string) => {
|
||||||
|
router.push(path).then(() => {
|
||||||
|
message.success(`已跳转到: ${path}`)
|
||||||
|
}).catch((error) => {
|
||||||
|
message.error(`跳转失败: ${error.message}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.menu-test {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-code) {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
x
Reference in New Issue
Block a user