diff --git a/src/api/index.ts b/src/api/index.ts index 267ec82..1ba32af 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -15,6 +15,10 @@ export { default as ExamApi } from './modules/exam' export { ChatApi } from './modules/chat' export { default as MessageApi } from './modules/message' export type { MessageItem, BackendMessageItem, SystemMessage } from './modules/message' +export { default as MenuApi } from './modules/menu' +export type { MenuItem } from './modules/menu' +export { SystemApi } from './modules/system' +export type { SystemSettings, DictItem } from './modules/system' // API 基础配置 export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/jeecgboot' @@ -246,6 +250,12 @@ export const API_ENDPOINTS = { DELETE: '/aiol/message/likes/:id', BATCH_DELETE: '/aiol/message/likes/batch-delete', }, + + // 菜单相关 + MENU: { + INDEX_MENUS: '/aiol/aiolMenu/getIndexMenus', + STUDENT_MENUS: '/aiol/aiolMenu/getStudentMenus', + }, // 资源相关 RESOURCES: { diff --git a/src/api/modules/system.ts b/src/api/modules/system.ts index 6972d58..dc0356b 100644 --- a/src/api/modules/system.ts +++ b/src/api/modules/system.ts @@ -9,6 +9,16 @@ export interface SystemSettings { maintenanceEndTime?: string } +// 字典项接口 +export interface DictItem { + value: string + text: string + color: string | null + jsonObject: any | null + label: string + title: string +} + export const SystemApi = { // 获取系统设置 getSystemSettings(): Promise> { @@ -28,5 +38,20 @@ export const SystemApi = { // 切换网站状态 toggleSiteStatus(enabled: boolean): Promise> { return request.post('/aiol/system/toggle', { enabled }) + }, + + // 获取字典项 + getDictItems(dictCode: string, params?: string): Promise> { + const timestamp = Date.now() + const url = params + ? `/sys/dict/getDictItems/${dictCode},${params}?_t=${timestamp}` + : `/sys/dict/getDictItems/${dictCode}?_t=${timestamp}` + return request.get(url) + }, + + // 获取AI模型字典项(LLM类型) + getAiModelDict(): Promise> { + const timestamp = Date.now() + return request.get(`/sys/dict/getDictItems/airag_model%20where%20model_type%20=%20'LLM',name,id?_t=${timestamp}`) } } diff --git a/src/api/request.ts b/src/api/request.ts index a238801..ece6a21 100644 --- a/src/api/request.ts +++ b/src/api/request.ts @@ -60,7 +60,10 @@ request.interceptors.request.use( } // 添加请求时间戳 - config.headers['X-Request-Time'] = Date.now().toString() + const timestamp = Date.now().toString() + config.headers['X-Request-Time'] = timestamp + config.headers['timestamp'] = timestamp + config.headers['X-Timestamp'] = timestamp // 开发环境下打印请求信息(已禁用) // if (import.meta.env.DEV) { diff --git a/src/components/SecondProject.vue b/src/components/SecondProject.vue index 4c3f807..ebfd172 100644 --- a/src/components/SecondProject.vue +++ b/src/components/SecondProject.vue @@ -11,10 +11,10 @@ 积分不足,需消耗29智点 -
-
- - 立即体验 +
+
+ + 立即体验
diff --git a/src/components/ThirdProject.vue b/src/components/ThirdProject.vue index 6232b81..d02fcd0 100644 --- a/src/components/ThirdProject.vue +++ b/src/components/ThirdProject.vue @@ -11,10 +11,10 @@ 积分不足,需消耗29智点
-
-
- - 立即体验 +
+
+ + 立即体验
diff --git a/src/router/index.ts b/src/router/index.ts index 7a59b28..ec2e0b9 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,5 +1,7 @@ import { createRouter, createWebHistory } from 'vue-router' import type { RouteRecordRaw } from 'vue-router' +import { useMenuStore } from '@/stores/menu' +import { generateRoutesFromMenus } from '@/utils/routeUtils' // ========== 前台页面组件 ========== import Home from '@/views/Home.vue' @@ -414,6 +416,12 @@ const routes: RouteRecordRaw[] = [ component: () => import('@/views/teacher/airag/ocr/AiOcrList.vue'), meta: { title: 'OCR识别' } }, + { + path: 'ai/app', + name: 'TeacherAiAppList', + component: () => import('@/views/Ai/AiAppList-NaiveUI.vue'), + meta: { title: 'AI应用管理' } + }, { path: 'student-management', name: 'StudentManagement', @@ -790,6 +798,18 @@ const routes: RouteRecordRaw[] = [ component: LocalVideoDemo, meta: { title: '本地视频播放演示' } }, + { + path: '/menu-test', + name: 'MenuTest', + component: () => import('@/views/MenuTest.vue'), + meta: { title: '菜单测试' } + }, + { + path: '/ai-model-test', + name: 'AiModelTest', + component: () => import('@/views/AiModelTest.vue'), + meta: { title: 'AI模型测试' } + }, // 网站维护页面 { @@ -848,10 +868,47 @@ const router = createRouter({ } }) +// ========== 动态路由加载 ========== +let isMenuLoaded = false + +/** + * 加载动态菜单路由 + */ +async function loadMenuRoutes() { + if (isMenuLoaded) return + + try { + const menuStore = useMenuStore() + await menuStore.fetchIndexMenus() + + // 生成动态路由 + const dynamicRoutes = generateRoutesFromMenus(menuStore.indexMenus) + + // 添加动态路由到路由器 + dynamicRoutes.forEach(route => { + // 检查路由是否已存在,避免重复添加 + if (!router.hasRoute(route.name as string)) { + router.addRoute(route) + console.log(`✅ 添加动态路由: ${route.path} -> ${String(route.name)}`) + } + }) + + isMenuLoaded = true + console.log('✅ 动态菜单路由加载完成') + } catch (error) { + console.error('❌ 加载动态菜单路由失败:', error) + } +} + // ========== 路由守卫 ========== import { maintenanceGuard } from '@/utils/maintenanceGuard' -router.beforeEach((to, from, next) => { +router.beforeEach(async (to, from, next) => { + // 加载动态菜单路由(仅在首次访问时) + if (!isMenuLoaded) { + await loadMenuRoutes() + } + // 设置页面标题 if (to.meta.title) { document.title = `${to.meta.title} - 在线学习平台` diff --git a/src/views/Ai.vue b/src/views/Ai.vue index b0b8c69..33c195a 100644 --- a/src/views/Ai.vue +++ b/src/views/Ai.vue @@ -8,7 +8,8 @@
实验方向 - 最新 最热 + 最新 + 最热 难度等级 @@ -19,26 +20,26 @@
- 经典技术 - 手势分类 + 经典技术 + 手势分类
-
+
在线训练
-
+
神经网络
-
+
特征提取 @@ -51,7 +52,7 @@
-
+
原理解释 @@ -61,25 +62,25 @@
-
+
大语言模型
-
+
声音识别
-
+
图像识别
-
+
生成式AI @@ -96,9 +97,11 @@
需消耗20智点
-
- - 立即体验 +
+
+ + 立即体验 +
@@ -113,10 +116,12 @@
还剩3次体验机会
-
- - 立即体验 +
+
+ + 立即体验 +
@@ -158,10 +163,12 @@ 积分不足,需消耗29智点
-
- - 立即体验 +
+
+ + 立即体验 +
@@ -179,11 +186,11 @@ src="https://lanhu-oss-proxy-lanhuapp.com/SketchPng36400ca1db866102585133601e1ce755c391054588a4f836a964e5d7ff91cb64" /> 积分不足,需消耗29智点
-
-
- +
+ - 立即体验 + 立即体验
@@ -201,11 +208,11 @@ src="https://lanhu-oss-proxy.lanhuapp.com/SketchPng36400ca1db866102585133601e1ce755c391054588a4f836a964e5d7ff91cb64" /> 积分不足,需消耗29智点
-
-
- +
+ - 立即体验 + 立即体验
@@ -250,13 +257,93 @@ const ICON_MAP: Record = { } const selectedTitle = ref('') +const sortType = ref<'latest' | 'hot'>('hot') // 默认选择"最热" +const selectedCategory = ref('') // 当前选择的分类 + +// 设置排序类型 +const setSortType = (type: 'latest' | 'hot') => { + sortType.value = type + filterAiApps() +} + +// 设置分类 +const setCategory = (category: string) => { + selectedCategory.value = selectedCategory.value === category ? '' : category + filterAiApps() +} + +// 筛选AI应用 +const filterAiApps = () => { + // 这里可以根据sortType和selectedCategory来筛选和排序右侧的AI应用 + console.log('当前排序:', sortType.value) + console.log('当前分类:', selectedCategory.value) + + // 可以在这里添加实际的筛选逻辑 + // 比如隐藏/显示不同的AI应用卡片 + updateVisibleApps() +} + +// 更新可见的应用 +const updateVisibleApps = () => { + // 获取所有AI应用卡片(包括组件) + const appCards = document.querySelectorAll('.group_18, .group_20, .group_23, .group_25, .group_26, .box_7, .box_8, .box_10') + + appCards.forEach((card, index) => { + const cardElement = card as HTMLElement + let shouldShow = true + + // 根据分类筛选 + if (selectedCategory.value) { + // 根据实际的应用分类来判断 + switch (selectedCategory.value) { + case '经典技术': + shouldShow = index < 3 // 前三个应用属于经典技术 + break + case '手势分类': + shouldShow = index === 0 || index === 3 // 手势相关应用 + break + case '在线训练': + shouldShow = index === 5 || index === 6 // 训练相关应用(FirstProject, SecondProject) + break + case '神经网络': + shouldShow = index === 6 || index === 7 // 神经网络相关应用(SecondProject, ThirdProject) + break + case '特征提取': + shouldShow = index === 1 || index === 4 // 特征提取相关应用 + break + case '原理解释': + shouldShow = index === 2 || index === 5 // 原理解释相关应用 + break + case '大语言模型': + shouldShow = index === 7 // GPT相关应用(ThirdProject) + break + case '声音识别': + shouldShow = index === 1 || index === 3 // 声音识别相关应用 + break + case '图像识别': + shouldShow = index === 0 || index === 2 // 图像识别相关应用 + break + case '生成式AI': + shouldShow = index === 5 || index === 7 // 生成式AI相关应用(FirstProject, ThirdProject) + break + default: + shouldShow = true + } + } + + // 显示或隐藏卡片 + cardElement.style.display = shouldShow ? 'flex' : 'none' + }) +} // 立即体验按钮跳转函数 const goToExperience = (index: number) => { const urls = [ 'http://103.40.14.23:25534', 'http://103.40.14.23:25535', - 'http://103.40.14.23:25536' + 'http://103.40.14.23:25536', + 'http://103.40.14.23:25537', + 'http://103.40.14.23:25538' ] if (index >= 0 && index < urls.length) { @@ -348,6 +435,23 @@ function hookFirstSecondThirdFourth() { function bindClicks() { getAllBlocks().forEach(el => { + // 跳过"立即体验"按钮和左侧分类按钮 + if (el.classList.contains('image-text_4') || + el.classList.contains('group_9') || + el.classList.contains('image-text_1') || + el.classList.contains('image-text_2') || + el.classList.contains('image-text_3') || + el.classList.contains('image-text_9') || + el.classList.contains('image-text_10') || + el.classList.contains('image-text_11') || + el.classList.contains('image-text_12') || + el.classList.contains('image-text_13') || + el.classList.contains('group_5') || + el.classList.contains('text_19') || + el.classList.contains('text_20')) { + return + } + const txt = el.querySelector('span')?.textContent || el.textContent || '' const title = resolveNavTitle(el, txt) el.classList.add('clickable') @@ -357,6 +461,19 @@ function bindClicks() { }) getNavBlocks().forEach(el => { + // 跳过左侧分类按钮 + if (el.classList.contains('image-text_1') || + el.classList.contains('image-text_2') || + el.classList.contains('image-text_3') || + el.classList.contains('image-text_9') || + el.classList.contains('image-text_10') || + el.classList.contains('image-text_11') || + el.classList.contains('image-text_12') || + el.classList.contains('image-text_13') || + el.classList.contains('group_5')) { + return + } + el.addEventListener('click', (e) => { const title = resolveNavTitle(el, (el as HTMLElement).innerText) setSelectionHighlight(title) @@ -872,20 +989,36 @@ button:active { white-space: nowrap; line-height: 20px; margin-left: 212px; + cursor: pointer; + transition: all 0.3s ease; +} + +.text_16.active { + color: rgba(0, 136, 209, 1); + font-family: PingFangSC-Medium; + font-weight: 500; } .text_17 { width: 32px; height: 20px; overflow-wrap: break-word; - color: rgba(0, 136, 209, 1); + color: rgba(51, 51, 51, 1); font-size: 16px; - font-family: PingFangSC-Medium; - font-weight: 500; + font-family: PingFangSC-Regular; + font-weight: normal; text-align: left; white-space: nowrap; line-height: 20px; margin-left: 32px; + cursor: pointer; + transition: all 0.3s ease; +} + +.text_17.active { + color: rgba(0, 136, 209, 1); + font-family: PingFangSC-Medium; + font-weight: 500; } .text_18 { @@ -947,6 +1080,13 @@ button:active { white-space: nowrap; line-height: 22px; margin-left: 16px; + cursor: pointer; + transition: all 0.3s ease; +} + +.text_19.active { + color: rgba(0, 136, 209, 1); + font-weight: 500; } .text_20 { @@ -961,6 +1101,12 @@ button:active { white-space: nowrap; line-height: 22px; margin-left: 176px; + cursor: pointer; + transition: all 0.3s ease; +} + +.text_20.active { + color: rgba(0, 136, 209, 1); } .group_6 { @@ -1006,6 +1152,30 @@ button:active { line-height: 22px; } +.image-text_1.active .text-group_1, +.image-text_2.active .text-group_2, +.image-text_3.active .text-group_3, +.image-text_9.active .text-group_12, +.image-text_10.active .text-group_13, +.image-text_11.active .text-group_14, +.image-text_12.active .text-group_15, +.image-text_13.active .text-group_16 { + color: rgba(0, 136, 209, 1); + font-weight: 500; +} + +.image-text_1, +.image-text_2, +.image-text_3, +.image-text_9, +.image-text_10, +.image-text_11, +.image-text_12, +.image-text_13 { + cursor: pointer; + transition: all 0.3s ease; +} + .image_8 { width: 214px; height: 1px; diff --git a/src/views/Ai/AiAppChat.vue b/src/views/Ai/AiAppChat.vue index 0286154..633fa68 100644 --- a/src/views/Ai/AiAppChat.vue +++ b/src/views/Ai/AiAppChat.vue @@ -102,7 +102,7 @@