This commit is contained in:
yuk255 2025-09-29 22:02:59 +08:00
commit 00ae37c216
16 changed files with 846 additions and 193 deletions

View File

@ -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: {

View File

@ -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<ApiResponse<SystemSettings>> {
@ -28,5 +38,20 @@ export const SystemApi = {
// 切换网站状态
toggleSiteStatus(enabled: boolean): Promise<ApiResponse<{ enabled: boolean }>> {
return request.post('/aiol/system/toggle', { enabled })
},
// 获取字典项
getDictItems(dictCode: string, params?: string): Promise<ApiResponse<DictItem[]>> {
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<ApiResponse<DictItem[]>> {
const timestamp = Date.now()
return request.get(`/sys/dict/getDictItems/airag_model%20where%20model_type%20=%20'LLM',name,id?_t=${timestamp}`)
}
}

View File

@ -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) {

View File

@ -11,10 +11,10 @@
<img class="thumbnail_10" referrerpolicy="no-referrer" src="https://lanhu-oss-proxy.lanhuapp.com/SketchPng36400ca1db866102585133601e1ce755c391054588a4f836a964e5d7ff91cb64" />
<span class="text-group_7">积分不足需消耗29智点</span>
</div>
<div class="box_9 flex-row">
<div class="image-text_6 flex-row justify-between">
<img class="thumbnail_11" referrerpolicy="no-referrer" src="https://lanhu-oss-proxy.lanhuapp.com/SketchPng8c408105a9fd19d2ed517d26c3972819e5802316449362c24f8fa8609583f1cf" />
<span class="text-group_8">立即体验</span>
<div class="group_9 flex-row">
<div class="image-text_4 flex-row justify-between">
<img class="thumbnail_9" referrerpolicy="no-referrer" src="https://lanhu-oss-proxy.lanhuapp.com/SketchPng8c408105a9fd19d2ed517d26c3972819e5802316449362c24f8fa8609583f1cf" />
<span class="text-group_5">立即体验</span>
</div>
</div>
</div>

View File

@ -11,10 +11,10 @@
<img class="thumbnail_12" referrerpolicy="no-referrer" src="https://lanhu-oss-proxy.lanhuapp.com/SketchPng36400ca1db866102585133601e1ce755c391054588a4f836a964e5d7ff91cb64" />
<span class="text-group_10">积分不足需消耗29智点</span>
</div>
<div class="group_10 flex-row">
<div class="image-text_8 flex-row justify-between">
<img class="thumbnail_13" referrerpolicy="no-referrer" src="https://lanhu-oss-proxy.lanhuapp.com/SketchPng8c408105a9fd19d2ed517d26c3972819e5802316449362c24f8fa8609583f1cf" />
<span class="text-group_11">立即体验</span>
<div class="group_9 flex-row">
<div class="image-text_4 flex-row justify-between">
<img class="thumbnail_9" referrerpolicy="no-referrer" src="https://lanhu-oss-proxy.lanhuapp.com/SketchPng8c408105a9fd19d2ed517d26c3972819e5802316449362c24f8fa8609583f1cf" />
<span class="text-group_5">立即体验</span>
</div>
</div>
</div>

View File

@ -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} - 在线学习平台`

View File

@ -8,7 +8,8 @@
</div>
<div class="group_3 flex-row">
<span class="text_15">实验方向</span>
<span class="text_16">最新</span> <span class="text_17">最热</span>
<span class="text_16" :class="{ active: sortType === 'latest' }" @click="setSortType('latest')">最新</span>
<span class="text_17" :class="{ active: sortType === 'hot' }" @click="setSortType('hot')">最热</span>
<span class="text_18">难度等级</span>
<img class="thumbnail_4" referrerpolicy="no-referrer"
src="https://lanhu-oss-proxy.lanhuapp.com/SketchPng20bb8df2d25963d2f9a15938bde5268726a9725a399eace8e8b30fa6f6fc6ae0" />
@ -19,26 +20,26 @@
<div class="group_5 flex-row">
<img class="thumbnail_5" referrerpolicy="no-referrer"
src="https://lanhu-oss-proxy.lanhuapp.com/SketchPng221d12611b979e5a2255fc34da1f37f982d562e7db29041e6d42e9528e14f249" />
<span class="text_19">经典技术</span>
<span class="text_20">手势分类</span>
<span class="text_19" @click.stop="setCategory('经典技术')" :class="{ active: selectedCategory === '经典技术' }">经典技术</span>
<span class="text_20" @click.stop="setCategory('手势分类')" :class="{ active: selectedCategory === '手势分类' }">手势分类</span>
</div>
<div class="group_6 flex-row">
<div class="box_5 flex-col">
<img class="image_7" referrerpolicy="no-referrer" src="/images/ai/55.jpg" />
<div class="image-text_1 flex-row justify-between">
<div class="image-text_1 flex-row justify-between" @click.stop="setCategory('在线训练')" :class="{ active: selectedCategory === '在线训练' }">
<img class="thumbnail_6" referrerpolicy="no-referrer"
src="https://lanhu-oss-proxy.lanhuapp.com/SketchPngd4359394ad3231a061a6c177963eff548f9dfad49670e0eeb1ea11aa3ac877c2" />
<span class="text-group_1">在线训练</span>
</div>
<img class="image_8" referrerpolicy="no-referrer" src="/images/ai/55.jpg" />
<div class="image-text_2 flex-row justify-between">
<div class="image-text_2 flex-row justify-between" @click.stop="setCategory('神经网络')" :class="{ active: selectedCategory === '神经网络' }">
<img class="thumbnail_7" referrerpolicy="no-referrer"
src="https://lanhu-oss-proxy.lanhuapp.com/SketchPng9c352004401a0cd1d0034f83919069ac4f46afee4c7472f36df34e0953cca68d" />
<span class="text-group_2">神经网络</span>
</div>
<img class="image_9" referrerpolicy="no-referrer" src="/images/ai/55.jpg" />
<div class="box_6 flex-row">
<div class="image-text_3 flex-row justify-between">
<div class="image-text_3 flex-row justify-between" @click.stop="setCategory('特征提取')" :class="{ active: selectedCategory === '特征提取' }">
<img class="thumbnail_8" referrerpolicy="no-referrer"
src="https://lanhu-oss-proxy.lanhuapp.com/SketchPng3eb646bd16341266f70c97c1b362075b4882c97778d0cce8b71dd3427cd2033c" />
<span class="text-group_3">特征提取</span>
@ -51,7 +52,7 @@
<ThirdProject />
</div>
<div class="group_11 flex-row justify-between">
<div class="image-text_9 flex-row justify-between">
<div class="image-text_9 flex-row justify-between" @click.stop="setCategory('原理解释')" :class="{ active: selectedCategory === '原理解释' }">
<img class="thumbnail_14" referrerpolicy="no-referrer"
src="https://lanhu-oss-proxy.lanhuapp.com/SketchPngae5cbb0288d044b7940a7e4f58e90a79b376525c7074fe6081d2eb80f3b66af2" />
<span class="text-group_12">原理解释</span>
@ -61,25 +62,25 @@
<img class="image_11" referrerpolicy="no-referrer" src="/images/ai/55.jpg" />
<div class="group_12 flex-row">
<div class="group_13 flex-col justify-between">
<div class="image-text_10 flex-row justify-between">
<div class="image-text_10 flex-row justify-between" @click.stop="setCategory('大语言模型')" :class="{ active: selectedCategory === '大语言模型' }">
<img class="thumbnail_15" referrerpolicy="no-referrer"
src="https://lanhu-oss-proxy.lanhuapp.com/SketchPngd3627550c0a611773c768561b0cc502ab1163b6e3b146e9488302ef4b7251277" />
<span class="text-group_13">大语言模型</span>
</div>
<img class="image_12" referrerpolicy="no-referrer" src="/images/ai/55.jpg" />
<div class="image-text_11 flex-row justify-between">
<div class="image-text_11 flex-row justify-between" @click.stop="setCategory('声音识别')" :class="{ active: selectedCategory === '声音识别' }">
<img class="thumbnail_16" referrerpolicy="no-referrer"
src="https://lanhu-oss-proxy.lanhuapp.com/SketchPngcc4df0afc94cd3f9f28423107ee9405bf78e0b8c065738718a8de09e514b3d9c" />
<span class="text-group_14">声音识别</span>
</div>
<img class="image_13" referrerpolicy="no-referrer" src="/images/ai/55.jpg" />
<div class="image-text_12 flex-row justify-between">
<div class="image-text_12 flex-row justify-between" @click.stop="setCategory('图像识别')" :class="{ active: selectedCategory === '图像识别' }">
<img class="thumbnail_17" referrerpolicy="no-referrer"
src="https://lanhu-oss-proxy.lanhuapp.com/SketchPng9c2b537e7b3537b38d16c6bf24dccbbbcc816a69760c86cc95c6c5075ee387c9" />
<span class="text-group_15">图像识别</span>
</div>
<img class="image_14" referrerpolicy="no-referrer" src="/images/ai/55.jpg" />
<div class="image-text_13 flex-row justify-between">
<div class="image-text_13 flex-row justify-between" @click.stop="setCategory('生成式AI')" :class="{ active: selectedCategory === '生成式AI' }">
<img class="thumbnail_18" referrerpolicy="no-referrer"
src="https://lanhu-oss-proxy.lanhuapp.com/SketchPng85eb232fb28be7bf813c024779fa564ca25755fce1f50bfcde00ed68af41ed04" />
<span class="text-group_16">生成式AI</span>
@ -96,9 +97,11 @@
<div class="box_12 flex-row justify-between">
<span class="text_34">需消耗20智点</span>
<div class="group_19 flex-row">
<div class="image-text_15 flex-row justify-between" @click="goToExperience(0)">
<img class="thumbnail_20" referrerpolicy="no-referrer" src="/images/ai/33.jpg" />
<span class="text-group_20 " >立即体验</span>
<div class="group_9 flex-row">
<div class="image-text_4 flex-row justify-between" @click.stop="goToExperience(0)">
<img class="thumbnail_9" referrerpolicy="no-referrer" src="https://lanhu-oss-proxy.lanhuapp.com/SketchPng8c408105a9fd19d2ed517d26c3972819e5802316449362c24f8fa8609583f1cf" />
<span class="text-group_5">立即体验</span>
</div>
</div>
</div>
</div>
@ -113,10 +116,12 @@
<div class="box_14 flex-row justify-between">
<span class="text_37">还剩3次体验机会</span>
<div class="box_15 flex-row">
<div class="image-text_16 flex-row justify-between" @click="goToExperience(1)">
<img class="thumbnail_21" referrerpolicy="no-referrer"
src="https://lanhu-oss-proxy.lanhuapp.com/SketchPng8c408105a9fd19d2ed517d26c3972819e5802316449362c24f8fa8609583f1cf" />
<span class="text-group_22">立即体验</span>
<div class="group_9 flex-row">
<div class="image-text_4 flex-row justify-between" @click.stop="goToExperience(1)">
<img class="thumbnail_9" referrerpolicy="no-referrer"
src="https://lanhu-oss-proxy.lanhuapp.com/SketchPng8c408105a9fd19d2ed517d26c3972819e5802316449362c24f8fa8609583f1cf" />
<span class="text-group_5">立即体验</span>
</div>
</div>
</div>
</div>
@ -158,10 +163,12 @@
<span class="text-group_27">积分不足需消耗29智点</span>
</div>
<div class="group_24 flex-row">
<div class="image-text_21 flex-row justify-between" @click="goToExperience(2)">
<img class="thumbnail_26" referrerpolicy="no-referrer"
src="https://lanhu-oss-proxy.lanhuapp.com/SketchPng8c408105a9fd19d2ed517d26c3972819e5802316449362c24f8fa8609583f1cf" />
<span class="text-group_28">立即体验</span>
<div class="group_9 flex-row">
<div class="image-text_4 flex-row justify-between" @click.stop="goToExperience(2)">
<img class="thumbnail_9" referrerpolicy="no-referrer"
src="https://lanhu-oss-proxy.lanhuapp.com/SketchPng8c408105a9fd19d2ed517d26c3972819e5802316449362c24f8fa8609583f1cf" />
<span class="text-group_5">立即体验</span>
</div>
</div>
</div>
</div>
@ -179,11 +186,11 @@
src="https://lanhu-oss-proxy-lanhuapp.com/SketchPng36400ca1db866102585133601e1ce755c391054588a4f836a964e5d7ff91cb64" />
<span class="text-group_30">积分不足需消耗29智点</span>
</div>
<div class="box_16 flex-row">
<div class="image-text_23 flex-row justify-between">
<img class="thumbnail_28" referrerpolicy="no-referrer"
<div class="group_9 flex-row">
<div class="image-text_4 flex-row justify-between" @click.stop="goToExperience(3)">
<img class="thumbnail_9" referrerpolicy="no-referrer"
src="https://lanhu-oss-proxy.lanhuapp.com/SketchPng8c408105a9fd19d2ed517d26c3972819e5802316449362c24f8fa8609583f1cf" />
<span class="text-group_31">立即体验</span>
<span class="text-group_5">立即体验</span>
</div>
</div>
</div>
@ -201,11 +208,11 @@
src="https://lanhu-oss-proxy.lanhuapp.com/SketchPng36400ca1db866102585133601e1ce755c391054588a4f836a964e5d7ff91cb64" />
<span class="text-group_33">积分不足需消耗29智点</span>
</div>
<div class="box_17 flex-row">
<div class="image-text_25 flex-row justify-between">
<img class="thumbnail_30" referrerpolicy="no-referrer"
<div class="group_9 flex-row">
<div class="image-text_4 flex-row justify-between" @click.stop="goToExperience(4)">
<img class="thumbnail_9" referrerpolicy="no-referrer"
src="https://lanhu-oss-proxy.lanhuapp.com/SketchPng8c408105a9fd19d2ed517d26c3972819e5802316449362c24f8fa8609583f1cf" />
<span class="text-group_34">立即体验</span>
<span class="text-group_5">立即体验</span>
</div>
</div>
</div>
@ -250,13 +257,93 @@ const ICON_MAP: Record<string, string> = {
}
const selectedTitle = ref<string>('')
const sortType = ref<'latest' | 'hot'>('hot') // ""
const selectedCategory = ref<string>('') //
//
const setSortType = (type: 'latest' | 'hot') => {
sortType.value = type
filterAiApps()
}
//
const setCategory = (category: string) => {
selectedCategory.value = selectedCategory.value === category ? '' : category
filterAiApps()
}
// AI
const filterAiApps = () => {
// sortTypeselectedCategoryAI
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 // GPTThirdProject
break
case '声音识别':
shouldShow = index === 1 || index === 3 //
break
case '图像识别':
shouldShow = index === 0 || index === 2 //
break
case '生成式AI':
shouldShow = index === 5 || index === 7 // AIFirstProject, 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;

View File

@ -102,7 +102,7 @@
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, nextTick } from 'vue'
import { ref, onMounted, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useMessage } from 'naive-ui'
import { ArrowLeftOutlined } from '@vicons/antd'

View File

@ -1,50 +1,44 @@
<template>
<div class="ai-app-container">
<!-- 查询区域 -->
<n-card class="search-card" :bordered="false">
<n-form
ref="searchFormRef"
:model="searchForm"
label-placement="left"
:label-width="80"
class="search-form"
>
<n-grid :cols="24" :x-gap="16" responsive="screen">
<n-form-item-gi :span="searchFormSpan.name" label="应用名称" path="name">
<n-input
v-model:value="searchForm.name"
placeholder="请输入应用名称"
clearable
@keyup.enter="handleSearch"
/>
</n-form-item-gi>
<n-form-item-gi :span="searchFormSpan.type" label="应用类型" path="type">
<n-select
v-model:value="searchForm.type"
placeholder="请选择应用类型"
clearable
:options="appTypeOptions"
/>
</n-form-item-gi>
<n-form-item-gi :span="searchFormSpan.actions">
<n-space>
<n-button type="primary" @click="handleSearch">
<template #icon>
<n-icon><SearchOutlined /></n-icon>
</template>
查询
</n-button>
<n-button @click="handleReset">
<template #icon>
<n-icon><ReloadOutlined /></n-icon>
</template>
重置
</n-button>
</n-space>
</n-form-item-gi>
</n-grid>
</n-form>
</n-card>
<div class="search-container">
<div class="search-form-wrapper">
<div class="search-item">
<label class="search-label">应用名称</label>
<n-input
v-model:value="searchForm.name"
placeholder="请输入应用名称"
clearable
class="search-input"
@keyup.enter="handleSearch"
/>
</div>
<div class="search-item">
<label class="search-label">应用类型</label>
<n-select
v-model:value="searchForm.type"
placeholder="请选择应用类型"
clearable
:options="appTypeOptions"
class="search-select"
/>
</div>
<div class="search-buttons">
<n-button type="primary" @click="handleSearch" class="search-btn">
<template #icon>
<n-icon><SearchOutlined /></n-icon>
</template>
查询
</n-button>
<n-button @click="handleReset" class="reset-btn">
<template #icon>
<n-icon><ReloadOutlined /></n-icon>
</template>
重置
</n-button>
</div>
</div>
</div>
<!-- 应用卡片区域 -->
<n-card class="app-cards-container" :bordered="false">
@ -158,7 +152,7 @@
<n-dropdown
:options="getActionOptions(item)"
@select="(key) => handleActionSelect(key, item)"
@select="(key: string) => handleActionSelect(key, item)"
>
<n-button text>
<template #icon>
@ -195,20 +189,92 @@
<n-modal
v-model:show="showAppModal"
preset="card"
:title="appModalTitle"
:style="modalStyle"
:mask-closable="false"
>
<AiAppForm
ref="appFormRef"
:form-data="currentApp"
:is-edit="isEditMode"
@submit="handleAppSubmit"
/>
<template #header>
<div class="modal-header">
<span class="modal-title">创建应用</span>
<n-icon size="16" class="help-icon">
<QuestionCircleOutlined />
</n-icon>
</div>
</template>
<div class="create-app-form">
<!-- 应用名称 -->
<div class="form-item">
<label class="form-label required">应用名称</label>
<n-input
v-model:value="appForm.name"
placeholder="请输入应用名称"
maxlength="64"
show-count
class="form-input"
/>
</div>
<!-- 应用描述 -->
<div class="form-item">
<label class="form-label">应用描述</label>
<n-input
v-model:value="appForm.description"
type="textarea"
placeholder="简述该应用的适用场景及用途"
maxlength="256"
show-count
:rows="4"
class="form-textarea"
/>
</div>
<!-- 应用图标 -->
<div class="form-item">
<label class="form-label">应用图标</label>
<div class="upload-area">
<div class="upload-box">
<n-icon size="24" class="upload-icon">
<UploadOutlined />
</n-icon>
<span class="upload-text">上传</span>
</div>
</div>
</div>
<!-- 选择应用类型 -->
<div class="form-item">
<label class="form-label">选择应用类型</label>
<div class="app-type-options">
<div
class="type-option"
:class="{ active: appForm.type === 'simple' }"
@click="appForm.type = 'simple'"
>
<n-radio :checked="appForm.type === 'simple'" />
<div class="type-content">
<div class="type-title">简单配置</div>
<div class="type-desc">适合新手创建小助手</div>
</div>
</div>
<div
class="type-option"
:class="{ active: appForm.type === 'advanced' }"
@click="appForm.type = 'advanced'"
>
<n-radio :checked="appForm.type === 'advanced'" />
<div class="type-content">
<div class="type-title">高级编排</div>
<div class="type-desc">适合高级用户自定义小助手的工作流</div>
</div>
</div>
</div>
</div>
</div>
<template #action>
<n-space>
<n-button @click="showAppModal = false">取消</n-button>
<n-button type="primary" @click="handleAppFormSubmit">确定</n-button>
<n-button type="primary" @click="handleCreateSubmit">确认</n-button>
</n-space>
</template>
</n-modal>
@ -257,17 +323,19 @@ import {
SendOutlined,
RocketOutlined,
GlobalOutlined,
MenuOutlined
MenuOutlined,
QuestionCircleOutlined,
UploadOutlined
} from '@vicons/antd'
import { aiAppApi } from './aiApp'
import type { AiApp, SearchForm, Pagination } from './type/aiApp'
import AiAppForm from './component/AiAppForm.vue'
// import AiAppForm from './component/AiAppForm.vue'
import AiAppSetting from './component/AiAppSetting.vue'
import AiAppPublish from './component/AiAppPublish.vue'
//
const searchFormRef = ref()
const appFormRef = ref()
// const appFormRef = ref()
const appSettingRef = ref()
const appPublishRef = ref()
@ -293,9 +361,35 @@ const showAppModal = ref(false)
const showSettingModal = ref(false)
const showPublishModal = ref(false)
const isEditMode = ref(false)
const currentApp = ref<Partial<AiApp>>({})
const currentApp = ref<AiApp>({
id: '',
name: '',
descr: '',
icon: '',
type: 'chatSimple',
status: 'enable',
modelId: '',
flowId: '',
knowledgeIds: '',
prompt: '',
prologue: '',
presetQuestion: '',
msgNum: 10,
createBy: '',
createTime: '',
updateBy: '',
updateTime: ''
})
const publishType = ref<'web' | 'menu'>('web')
//
const appForm = reactive({
name: '',
description: '',
icon: '',
type: 'simple' // simple | advanced
})
//
const appTypeOptions = [
{ label: '简单配置', value: 'chatSimple' },
@ -303,7 +397,7 @@ const appTypeOptions = [
]
//
const appModalTitle = computed(() => isEditMode.value ? '编辑应用' : '创建应用')
// const appModalTitle = computed(() => isEditMode.value ? '' : '')
const publishModalTitle = computed(() => publishType.value === 'web' ? '嵌入网站' : '配置菜单')
//
@ -322,19 +416,19 @@ const gridSpan = computed(() => {
})
//
const searchFormSpan = computed(() => {
if (typeof window !== 'undefined') {
const width = window.innerWidth
if (width >= 1200) {
return { name: 6, type: 6, actions: 6 } //
}
if (width >= 768) {
return { name: 8, type: 8, actions: 8 } //
}
return { name: 24, type: 24, actions: 24 } //
}
return { name: 6, type: 6, actions: 6 } //
})
// const searchFormSpan = computed(() => {
// if (typeof window !== 'undefined') {
// const width = window.innerWidth
// if (width >= 1200) {
// return { name: 6, type: 6, actions: 6 } //
// }
// if (width >= 768) {
// return { name: 8, type: 8, actions: 8 } //
// }
// return { name: 24, type: 24, actions: 24 } //
// }
// return { name: 6, type: 6, actions: 6 } //
// })
//
const modalStyle = computed(() => {
@ -375,7 +469,17 @@ const publishModalStyle = computed(() => {
//
const getAppIcon = (icon?: string) => {
return icon ? `/api/sys/common/static/${icon}` : '/default-app-icon.png'
if (!icon) {
return '/default-app-icon.png'
}
// HTTP URL
if (icon.startsWith('http://') || icon.startsWith('https://')) {
return icon
}
//
return `/api/sys/common/static/${icon}`
}
//
@ -422,7 +526,7 @@ const loadAppList = async () => {
pageNo: pagination.page,
pageSize: pagination.pageSize,
column: 'createTime',
order: 'desc',
order: 'desc' as 'desc',
...searchForm
}
@ -469,10 +573,58 @@ const handlePageSizeChange = (pageSize: number) => {
//
const handleCreateApp = () => {
isEditMode.value = false
currentApp.value = {}
currentApp.value = {
id: '',
name: '',
descr: '',
icon: '',
type: 'chatSimple',
status: 'enable',
modelId: '',
flowId: '',
knowledgeIds: '',
prompt: '',
prologue: '',
presetQuestion: '',
msgNum: 10,
createBy: '',
createTime: '',
updateBy: '',
updateTime: ''
}
//
appForm.name = ''
appForm.description = ''
appForm.icon = ''
appForm.type = 'simple'
showAppModal.value = true
}
//
const handleCreateSubmit = async () => {
if (!appForm.name.trim()) {
message.error('请输入应用名称')
return
}
try {
const formData = {
name: appForm.name,
description: appForm.description,
icon: appForm.icon,
type: (appForm.type === 'simple' ? 'chatSimple' : 'chatFlow') as 'chatSimple' | 'chatFlow'
}
await aiAppApi.saveApp(formData)
message.success('创建成功')
showAppModal.value = false
loadAppList()
} catch (error) {
message.error('创建失败')
console.error('Create app error:', error)
}
}
//
const handleEditApp = (app: AiApp) => {
isEditMode.value = true
@ -560,28 +712,28 @@ const handleRelease = (app: AiApp, toRelease: boolean) => {
}
//
const handleAppFormSubmit = () => {
appFormRef.value?.submit()
}
// const handleAppFormSubmit = () => {
// appFormRef.value?.submit()
// }
const handleAppSubmit = async (formData: AiApp) => {
try {
await aiAppApi.saveApp(formData)
message.success(isEditMode.value ? '编辑成功' : '创建成功')
showAppModal.value = false
if (!isEditMode.value) {
//
currentApp.value = formData
showSettingModal.value = true
}
loadAppList()
} catch (error) {
message.error(isEditMode.value ? '编辑失败' : '创建失败')
console.error('Save app error:', error)
}
}
// const handleAppSubmit = async (formData: AiApp) => {
// try {
// await aiAppApi.saveApp(formData)
// message.success(isEditMode.value ? '' : '')
// showAppModal.value = false
//
// if (!isEditMode.value) {
// //
// currentApp.value = formData
// showSettingModal.value = true
// }
//
// loadAppList()
// } catch (error) {
// message.error(isEditMode.value ? '' : '')
// console.error('Save app error:', error)
// }
// }
//
const handleSettingSuccess = () => {
@ -610,23 +762,89 @@ onMounted(() => {
padding: 12px;
}
.search-card {
margin-bottom: 16px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
.search-container {
margin-bottom: 24px;
background: transparent;
.search-form {
.n-form-item {
margin-bottom: 0;
.search-form-wrapper {
display: flex;
align-items: center;
gap: 24px;
flex-wrap: wrap;
.search-item {
display: flex;
align-items: center;
gap: 8px;
.search-label {
font-size: 14px;
color: #666;
white-space: nowrap;
min-width: 70px;
}
.search-input,
.search-select {
width: 300px;
}
}
.search-buttons {
display: flex;
gap: 12px;
margin-left: 0;
.search-btn {
background: #1890ff;
border: none;
border-radius: 4px;
padding: 0 16px;
height: 32px;
}
.reset-btn {
background: #f5f5f5;
border: 1px solid #d9d9d9;
border-radius: 4px;
padding: 0 16px;
height: 32px;
color: #666;
}
}
//
@media (max-width: 768px) {
flex-direction: column;
align-items: stretch;
gap: 16px;
.search-item {
flex-direction: column;
align-items: stretch;
.search-label {
min-width: auto;
}
.search-input,
.search-select {
width: 100%;
}
}
.search-buttons {
margin-left: 0;
justify-content: center;
}
}
}
}
.app-cards-container {
background: white;
background: transparent !important;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
box-shadow: none !important;
.add-app-card {
height: 180px;
@ -638,6 +856,7 @@ onMounted(() => {
background: #fafafa;
border-radius: 8px;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
//
@media (max-width: 768px) {
@ -651,8 +870,12 @@ onMounted(() => {
&:hover {
border-color: #1890ff;
background-color: #f0f8ff;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
.add-app-content {
display: flex;
flex-direction: column;
@ -680,7 +903,9 @@ onMounted(() => {
transition: all 0.3s ease;
position: relative;
border-radius: 8px;
border: 1px solid #f0f0f0;
border: none;
background: transparent;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
//
@media (max-width: 768px) {
@ -691,11 +916,7 @@ onMounted(() => {
height: 140px;
}
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
border-color: #1890ff;
}
.app-header {
display: flex;
@ -773,6 +994,10 @@ onMounted(() => {
}
:deep(.n-card) {
background: transparent !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important;
border: none !important;
.n-card__content {
height: 100%;
display: flex;
@ -793,4 +1018,134 @@ onMounted(() => {
background-color: rgba(24, 144, 255, 0.1);
}
}
//
:deep(.app-cards-container.n-card) {
background: transparent !important;
box-shadow: none !important;
border: none !important;
}
:deep(.app-cards-container .n-card__content) {
background: transparent !important;
}
//
.modal-header {
display: flex;
align-items: center;
gap: 8px;
.modal-title {
font-size: 16px;
font-weight: 500;
color: #333;
}
.help-icon {
color: #999;
cursor: pointer;
}
}
.create-app-form {
.form-item {
margin-bottom: 24px;
.form-label {
display: block;
margin-bottom: 8px;
font-size: 14px;
color: #333;
font-weight: 500;
&.required::before {
content: '*';
color: #ff4d4f;
margin-right: 4px;
}
}
.form-input,
.form-textarea {
width: 100%;
}
}
.upload-area {
.upload-box {
width: 80px;
height: 80px;
border: 1px dashed #d9d9d9;
border-radius: 6px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
border-color: #1890ff;
background-color: #f0f8ff;
}
.upload-icon {
color: #999;
margin-bottom: 4px;
}
.upload-text {
font-size: 12px;
color: #999;
}
}
}
.app-type-options {
display: flex;
gap: 16px;
.type-option {
flex: 1;
padding: 16px;
border: 1px solid #e8e8e8;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: flex-start;
gap: 12px;
&:hover {
border-color: #1890ff;
background-color: #f0f8ff;
}
&.active {
border-color: #1890ff;
background-color: #f0f8ff;
}
.type-content {
flex: 1;
.type-title {
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 4px;
}
.type-desc {
font-size: 12px;
color: #666;
line-height: 1.4;
}
}
}
}
}
</style>

View File

@ -73,7 +73,7 @@ export const aiAppApi = {
*
* @param params
*/
generatePrompt(params: { prompt: string }): Promise<ReadableStream> {
generatePrompt(params: { prompt: string }): Promise<any> {
return http.post(`/airag/app/prompt/generate?prompt=${encodeURIComponent(params.prompt)}`, null, {
responseType: 'stream',
timeout: 5 * 60 * 1000

View File

@ -87,7 +87,7 @@
class="type-option-card"
:class="{ active: formData.type === option.value }"
hoverable
@click="formData.type = option.value"
@click="formData.type = option.value as 'chatSimple' | 'chatFlow'"
>
<div class="type-option-content">
<n-radio :value="option.value" />
@ -109,7 +109,7 @@
</template>
<script setup lang="ts">
import { ref, reactive, watch, computed } from 'vue'
import { ref, reactive, watch } from 'vue'
import { useMessage } from 'naive-ui'
import { CloudUploadOutlined, RobotOutlined, SettingOutlined } from '@vicons/antd'
import type { FormInst, FormRules, UploadFileInfo } from 'naive-ui'
@ -141,6 +141,7 @@ const formData = reactive<AppFormData>({
descr: '',
icon: '',
type: 'chatSimple',
status: 'enable',
...props.formData
})
@ -204,7 +205,7 @@ const handleBeforeUpload = (data: { file: UploadFileInfo }) => {
}
//
const handleUploadFinish = async ({ file, event }: { file: UploadFileInfo, event?: ProgressEvent }) => {
const handleUploadFinish = async ({ file }: { file: UploadFileInfo, event?: ProgressEvent }) => {
try {
if (file.file) {
const response = await uploadApi.uploadImage(file.file)

View File

@ -208,11 +208,11 @@ const menuConfig = reactive({
})
//
const themeOptions = [
{ label: '浅色主题', value: 'light' },
{ label: '深色主题', value: 'dark' },
{ label: '自动主题', value: 'auto' }
]
// const themeOptions = [
// { label: '', value: 'light' },
// { label: '', value: 'dark' },
// { label: '', value: 'auto' }
// ]
// URL
const previewUrl = computed(() => {

View File

@ -279,7 +279,8 @@
>
<n-form-item label="选择流程" path="flowId">
<n-select
v-model:value="configData.flow.flowId"
:value="configData.flow?.flowId"
@update:value="(value: string) => { if (configData.flow) configData.flow.flowId = value }"
placeholder="请选择流程"
:options="flowOptions"
:loading="loadingFlows"
@ -288,7 +289,7 @@
</n-form-item>
</n-form>
<div v-if="configData.flow.flowId" class="flow-preview">
<div v-if="configData.flow?.flowId" class="flow-preview">
<h4>流程预览</h4>
<n-card>
<n-empty description="流程预览功能开发中" />
@ -327,6 +328,7 @@ import {
import type { FormInst } from 'naive-ui'
import type { AiApp, AppConfig, Knowledge } from '../type/aiApp'
import { aiAppApi } from '../aiApp'
import { SystemApi } from '@/api'
interface Props {
appData: AiApp
@ -356,8 +358,8 @@ const generatingPrompt = ref(false)
const showKnowledgeModal = ref(false)
//
const modelOptions = ref([])
const flowOptions = ref([])
const modelOptions = ref<Array<{label: string, value: string}>>([])
const flowOptions = ref<Array<{label: string, value: string}>>([])
const selectedKnowledges = ref<Knowledge[]>([])
//
@ -422,21 +424,45 @@ const flowRules = {
const loadModelOptions = async () => {
loadingModels.value = true
try {
// API
// const response = await aiModelApi.getModelList()
// modelOptions.value = response.result.map(item => ({
// label: item.name,
// value: item.id
// }))
//
console.log('🚀 开始加载AI模型选项...')
console.log('🔍 当前时间戳:', Date.now())
const response = await SystemApi.getAiModelDict()
console.log('📦 AI模型API完整响应:', response)
console.log('📦 响应数据结构:', {
code: response.code,
data: response.data,
dataType: typeof response.data,
dataKeys: response.data ? Object.keys(response.data) : []
})
if (response.code === 200 || response.code === 0) {
const dataList = response.data || []
modelOptions.value = dataList.map((item: any) => ({
label: item.text,
value: item.value
}))
console.log('✅ AI模型列表加载成功:', modelOptions.value)
} else {
throw new Error(response.message || '获取模型列表失败')
}
} catch (error: any) {
console.error('❌ 加载模型列表失败:', error)
console.error('❌ 错误详情:', {
message: error.message,
response: error.response,
config: error.config
})
message.error(error.message || '加载模型列表失败')
// 使
modelOptions.value = [
{ label: 'GPT-3.5 Turbo', value: 'gpt-3.5-turbo' },
{ label: 'GPT-4', value: 'gpt-4' },
{ label: 'Claude-3', value: 'claude-3' }
]
} catch (error) {
message.error('加载模型列表失败')
} finally {
loadingModels.value = false
}
@ -493,7 +519,7 @@ const handleModelChange = (modelId: string) => {
'claude-3': { temperature: 0.7, maxTokens: 4000 }
}
const defaults = modelDefaults[modelId]
const defaults = modelDefaults[modelId as keyof typeof modelDefaults]
if (defaults) {
Object.assign(configData.model, defaults)
}
@ -568,7 +594,8 @@ const handleSave = async () => {
prologue: configData.chat.prologue,
presetQuestion: configData.chat.presetQuestions?.join('\n') || '',
msgNum: configData.chat.msgNum,
flowId: configData.flow?.flowId || ''
flowId: configData.flow?.flowId || '',
type: props.appData.type
}
await aiAppApi.saveApp(saveData)

View File

@ -60,6 +60,8 @@ export interface AiApp {
createBy_dictText?: string
/** 创建时间 */
createTime?: string
/** 更新者 */
updateBy?: string
/** 更新时间 */
updateTime?: string
}

View File

@ -33,7 +33,7 @@ class HttpClient {
constructor(config?: AxiosRequestConfig) {
this.instance = axios.create({
baseURL: '/api',
baseURL: '/jeecgboot',
timeout: 60000,
headers: {
'Content-Type': 'application/json;charset=UTF-8'
@ -55,7 +55,6 @@ class HttpClient {
const token = this.getToken()
if (token) {
config.headers = config.headers || {}
config.headers['Authorization'] = `Bearer ${token}`
config.headers['X-Access-Token'] = token
}
@ -89,14 +88,14 @@ class HttpClient {
if (requestConfig.showSuccessMessage && requestConfig.successMessage) {
this.message.success(requestConfig.successMessage)
}
return data
return response
} else {
// 处理业务错误
const errorMessage = data.message || '请求失败'
if (requestConfig.showErrorMessage !== false) {
this.message.error(errorMessage)
}
return Promise.reject(new Error(errorMessage))
return Promise.reject(response)
}
},
(error: AxiosError) => {
@ -143,7 +142,7 @@ class HttpClient {
* token
*/
private getToken(): string | null {
return localStorage.getItem('ACCESS_TOKEN') || sessionStorage.getItem('ACCESS_TOKEN')
return localStorage.getItem('X-Access-Token') || sessionStorage.getItem('X-Access-Token')
}
/**
@ -157,12 +156,16 @@ class HttpClient {
*
*/
private handleUnauthorized() {
console.log('401 Unauthorized - Current token:', this.getToken())
console.log('Current path:', window.location.pathname)
// 清除token
localStorage.removeItem('ACCESS_TOKEN')
sessionStorage.removeItem('ACCESS_TOKEN')
// 跳转到登录页
if (window.location.pathname !== '/login') {
console.log('Redirecting to login page...')
window.location.href = '/login'
}
}
@ -249,7 +252,7 @@ class HttpClient {
export const http = new HttpClient()
// 导出类型
export type { HttpResponse, HttpRequestConfig }
// export type { HttpResponse, HttpRequestConfig }
export { HttpClient }
export default http

View File

@ -62,17 +62,17 @@
</div>
<!-- ai助教 - 已注释 -->
<!-- <div class="ai-container">
<div class="ai-container">
<router-link to="/teacher/ai-assistant" class="ai-tab" @mouseenter="isAiHovered = true"
@mouseleave="isAiHovered = false">
<img :src="(isAiActive || isAiHovered) ? '/images/aiAssistant/AI助教1.png' : '/images/aiAssistant/AI助教2.png'"
alt="ai" />
<span>AI助教</span>
</router-link>
</div> -->
</div>
<!-- 智能体编排 - 可展开菜单 - 已注释 -->
<!-- <div class="nav-container orchestration-nav">
<div class="nav-container orchestration-nav">
<div class="nav-item" :class="{ active: activeNavItem === 6 }" @click="toggleOrchestrationMenu">
<img :src="activeNavItem === 6 ? '/images/aiAssistant/AI助教1.png' : '/images/aiAssistant/AI助教2.png'" alt="">
<span>智能体编排</span>
@ -82,7 +82,7 @@
</div>
<div class="submenu-container" :class="{ expanded: orchestrationMenuExpanded }">
<router-link to="/teacher/airag/aiapp" class="submenu-item"
<router-link to="/teacher/ai/app" class="submenu-item"
:class="{ active: activeSubNavItem === 'app-management' }" @click="setActiveSubNavItem('app-management')">
<span>AI应用管理</span>
</router-link>
@ -104,7 +104,7 @@
<span>OCR识别</span>
</router-link>
</div>
</div> -->
</div>
</div>
<!-- 右侧路由视图 -->