Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1d7079e779 |
@ -1,8 +1,6 @@
|
|||||||
---
|
---
|
||||||
type: "always_apply"
|
type: "manual"
|
||||||
description: "Example description"
|
|
||||||
---
|
---
|
||||||
|
|
||||||
1、在接下来的每一个步骤当中,请帮我实现对页面的响应式设计
|
1、在接下来的每一个步骤当中,请帮我实现对页面的响应式设计
|
||||||
2、必须严格执行我给你的指令,一步一步执行,不得有缩减
|
2、必须严格执行我给你的指令,一步一步执行,不得有缩减
|
||||||
3、我们用的是naive UI组件 ,TS,vue3
|
|
||||||
|
2
.env
@ -1,5 +1,5 @@
|
|||||||
# API配置
|
# API配置
|
||||||
VITE_API_BASE_URL=http://103.40.14.23:25526/jeecgboot
|
VITE_API_BASE_URL=http://110.42.96.65:55510/api
|
||||||
|
|
||||||
# Mock配置 - 禁用Mock,使用真实API
|
# Mock配置 - 禁用Mock,使用真实API
|
||||||
VITE_ENABLE_MOCK=false
|
VITE_ENABLE_MOCK=false
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# 开发环境配置
|
# 开发环境配置
|
||||||
|
|
||||||
# API配置
|
# API配置
|
||||||
VITE_API_BASE_URL=http://103.40.14.23:25526/jeecgboot
|
VITE_API_BASE_URL=http://110.42.96.65:55510/api
|
||||||
|
|
||||||
# Mock配置
|
# Mock配置
|
||||||
# 设置为 true 使用Mock数据,false 使用真实API
|
# 设置为 true 使用Mock数据,false 使用真实API
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# 生产环境配置
|
# 生产环境配置
|
||||||
|
|
||||||
# API配置
|
# API配置
|
||||||
VITE_API_BASE_URL=http://103.40.14.23:25526/jeecgboot
|
VITE_API_BASE_URL=http://110.42.96.65:55510/api
|
||||||
|
|
||||||
# Mock配置 - 生产环境禁用Mock,使用真实API
|
# Mock配置 - 生产环境禁用Mock,使用真实API
|
||||||
VITE_ENABLE_MOCK=false
|
VITE_ENABLE_MOCK=false
|
||||||
|
@ -6,6 +6,6 @@ services:
|
|||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "25527:80"
|
- "55514:80"
|
||||||
container_name: vue3-nginx
|
container_name: vue3-nginx
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
@ -1,320 +0,0 @@
|
|||||||
# DPlayer 集成指南
|
|
||||||
|
|
||||||
## 什么是 DPlayer?
|
|
||||||
|
|
||||||
**DPlayer** 是由 [DIYGod](https://github.com/DIYGod) 开发的一个开源的 HTML5 视频播放器,具有以下特点:
|
|
||||||
|
|
||||||
- 🎨 **界面美观**:现代化的设计风格
|
|
||||||
- 🎯 **轻量级**:体积小,加载快
|
|
||||||
- 🌏 **中文友好**:由中国开发者开发,中文文档完善
|
|
||||||
- 🎮 **功能丰富**:支持弹幕、快捷键、倍速播放等
|
|
||||||
- 📱 **移动端适配**:响应式设计,支持移动设备
|
|
||||||
|
|
||||||
## 主要功能特性
|
|
||||||
|
|
||||||
### 基础功能
|
|
||||||
- ✅ 播放/暂停控制
|
|
||||||
- ✅ 音量控制
|
|
||||||
- ✅ 进度条拖拽
|
|
||||||
- ✅ 全屏切换
|
|
||||||
- ✅ 倍速播放 (0.5x - 2x)
|
|
||||||
|
|
||||||
### 高级功能
|
|
||||||
- 🎯 键盘快捷键支持
|
|
||||||
- 🎨 自定义主题色
|
|
||||||
- 📝 右键菜单自定义
|
|
||||||
- 🎵 音频可视化
|
|
||||||
- 📱 移动端手势支持
|
|
||||||
|
|
||||||
### 格式支持
|
|
||||||
- MP4
|
|
||||||
- WebM
|
|
||||||
- Ogg
|
|
||||||
- HLS (.m3u8)
|
|
||||||
- FLV
|
|
||||||
- 更多格式通过插件支持
|
|
||||||
|
|
||||||
## 安装和集成
|
|
||||||
|
|
||||||
### 方法1:CDN 引入(推荐用于快速测试)
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!-- 在 index.html 中引入 -->
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/dplayer@1.27.1/dist/DPlayer.min.css">
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/dplayer@1.27.1/dist/DPlayer.min.js"></script>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 方法2:NPM 安装(推荐用于生产环境)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install dplayer
|
|
||||||
```
|
|
||||||
|
|
||||||
然后在组件中导入:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import DPlayer from 'dplayer'
|
|
||||||
import 'dplayer/dist/DPlayer.min.css'
|
|
||||||
```
|
|
||||||
|
|
||||||
## 基础使用
|
|
||||||
|
|
||||||
### 创建播放器
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const player = new DPlayer({
|
|
||||||
container: document.getElementById('dplayer'),
|
|
||||||
video: {
|
|
||||||
url: 'video.mp4',
|
|
||||||
type: 'auto'
|
|
||||||
},
|
|
||||||
autoplay: false,
|
|
||||||
theme: '#007bff',
|
|
||||||
lang: 'zh-cn'
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 事件监听
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
player.on('play', () => {
|
|
||||||
console.log('视频开始播放')
|
|
||||||
})
|
|
||||||
|
|
||||||
player.on('pause', () => {
|
|
||||||
console.log('视频暂停')
|
|
||||||
})
|
|
||||||
|
|
||||||
player.on('ended', () => {
|
|
||||||
console.log('视频播放结束')
|
|
||||||
})
|
|
||||||
|
|
||||||
player.on('error', () => {
|
|
||||||
console.log('播放出错')
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## 配置选项
|
|
||||||
|
|
||||||
### 基础配置
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const options = {
|
|
||||||
container: document.getElementById('dplayer'), // 容器元素
|
|
||||||
video: {
|
|
||||||
url: 'video.mp4', // 视频地址
|
|
||||||
type: 'auto', // 视频类型:auto, normal, hls, flv
|
|
||||||
defaultQuality: 0, // 默认画质
|
|
||||||
pic: 'poster.jpg', // 封面图
|
|
||||||
thumbnails: 'thumbnails.jpg' // 缩略图
|
|
||||||
},
|
|
||||||
autoplay: false, // 自动播放
|
|
||||||
theme: '#007bff', // 主题色
|
|
||||||
lang: 'zh-cn', // 语言:zh-cn, en
|
|
||||||
hotkey: true, // 启用快捷键
|
|
||||||
preload: 'auto', // 预加载:auto, metadata, none
|
|
||||||
volume: 0.8, // 默认音量
|
|
||||||
playbackSpeed: [0.5, 0.75, 1, 1.25, 1.5, 2], // 倍速选项
|
|
||||||
contextmenu: [ // 右键菜单
|
|
||||||
{
|
|
||||||
text: '关于 DPlayer',
|
|
||||||
link: 'https://github.com/DIYGod/DPlayer'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 高级配置
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const advancedOptions = {
|
|
||||||
// 弹幕配置
|
|
||||||
danmaku: {
|
|
||||||
id: 'dplayer-danmaku',
|
|
||||||
api: 'https://api.prprpr.me/dplayer/',
|
|
||||||
token: 'token',
|
|
||||||
maximum: 1000,
|
|
||||||
addition: ['https://api.prprpr.me/dplayer/bilibili?aid=4157142'],
|
|
||||||
user: 'DIYGod',
|
|
||||||
bottom: '15%',
|
|
||||||
unlimited: true
|
|
||||||
},
|
|
||||||
|
|
||||||
// 字幕配置
|
|
||||||
subtitle: {
|
|
||||||
url: 'subtitle.vtt',
|
|
||||||
type: 'webvtt',
|
|
||||||
fontSize: '20px',
|
|
||||||
bottom: '10%',
|
|
||||||
color: '#fff'
|
|
||||||
},
|
|
||||||
|
|
||||||
// 画质切换
|
|
||||||
video: {
|
|
||||||
url: [
|
|
||||||
{
|
|
||||||
name: '1080P',
|
|
||||||
url: 'video-1080p.mp4'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '720P',
|
|
||||||
url: 'video-720p.mp4'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
defaultQuality: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 在 Vue 项目中使用
|
|
||||||
|
|
||||||
### 创建 DPlayer 组件
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<template>
|
|
||||||
<div class="video-player-wrapper">
|
|
||||||
<div ref="dplayerContainer" class="dplayer-container"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
|
||||||
|
|
||||||
const dplayerContainer = ref()
|
|
||||||
let player = null
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// 确保 DPlayer 已加载
|
|
||||||
if (window.DPlayer) {
|
|
||||||
initPlayer()
|
|
||||||
} else {
|
|
||||||
loadDPlayer().then(() => {
|
|
||||||
initPlayer()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const loadDPlayer = () => {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const cssLink = document.createElement('link')
|
|
||||||
cssLink.rel = 'stylesheet'
|
|
||||||
cssLink.href = 'https://cdn.jsdelivr.net/npm/dplayer@1.27.1/dist/DPlayer.min.css'
|
|
||||||
document.head.appendChild(cssLink)
|
|
||||||
|
|
||||||
const script = document.createElement('script')
|
|
||||||
script.src = 'https://cdn.jsdelivr.net/npm/dplayer@1.27.1/dist/DPlayer.min.js'
|
|
||||||
script.onload = resolve
|
|
||||||
document.head.appendChild(script)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const initPlayer = () => {
|
|
||||||
player = new window.DPlayer({
|
|
||||||
container: dplayerContainer.value,
|
|
||||||
video: {
|
|
||||||
url: '/video/first.mp4',
|
|
||||||
type: 'auto'
|
|
||||||
},
|
|
||||||
autoplay: false,
|
|
||||||
theme: '#007bff',
|
|
||||||
lang: 'zh-cn',
|
|
||||||
hotkey: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (player) {
|
|
||||||
player.destroy()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.video-player-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dplayer-container {
|
|
||||||
width: 100%;
|
|
||||||
aspect-ratio: 16/9;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 样式定制
|
|
||||||
|
|
||||||
### 自定义主题色
|
|
||||||
|
|
||||||
```css
|
|
||||||
/* 修改播放器主题色 */
|
|
||||||
.dplayer {
|
|
||||||
--dplayer-theme: #007bff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 自定义进度条颜色 */
|
|
||||||
.dplayer .dplayer-bar-wrap .dplayer-bar .dplayer-played {
|
|
||||||
background: #007bff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 自定义控制按钮颜色 */
|
|
||||||
.dplayer .dplayer-icons .dplayer-icon {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dplayer .dplayer-icons .dplayer-icon:hover {
|
|
||||||
color: #007bff;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 响应式设计
|
|
||||||
|
|
||||||
```css
|
|
||||||
/* 移动端适配 */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.dplayer {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dplayer .dplayer-icons .dplayer-icon {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 快捷键支持
|
|
||||||
|
|
||||||
DPlayer 默认支持以下快捷键:
|
|
||||||
|
|
||||||
- `空格键` - 播放/暂停
|
|
||||||
- `←` - 后退 10 秒
|
|
||||||
- `→` - 前进 10 秒
|
|
||||||
- `↑` - 音量 +10%
|
|
||||||
- `↓` - 音量 -10%
|
|
||||||
- `F` - 全屏切换
|
|
||||||
- `M` - 静音切换
|
|
||||||
|
|
||||||
## 与 CKPlayer 对比
|
|
||||||
|
|
||||||
| 特性 | CKPlayer | DPlayer |
|
|
||||||
|------|----------|---------|
|
|
||||||
| 界面美观度 | ⭐⭐ | ⭐⭐⭐⭐⭐ |
|
|
||||||
| 功能丰富度 | ⭐⭐⭐ | ⭐⭐⭐⭐ |
|
|
||||||
| 移动端支持 | ⭐⭐⭐ | ⭐⭐⭐⭐ |
|
|
||||||
| 中文支持 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
|
|
||||||
| 社区活跃度 | ⭐⭐ | ⭐⭐⭐⭐ |
|
|
||||||
| 文档质量 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
|
|
||||||
| 学习成本 | ⭐⭐⭐⭐ | ⭐⭐⭐ |
|
|
||||||
|
|
||||||
## 总结
|
|
||||||
|
|
||||||
**DPlayer 是一个优秀的视频播放器选择**,特别适合:
|
|
||||||
|
|
||||||
- 🎯 需要美观界面的项目
|
|
||||||
- 🌏 中文用户群体
|
|
||||||
- 📱 重视移动端体验
|
|
||||||
- 🎨 需要自定义主题的项目
|
|
||||||
- ⚡ 追求轻量级解决方案
|
|
||||||
|
|
||||||
相比当前的 CKPlayer,DPlayer 提供了更好的用户体验和更丰富的功能,是升级视频播放器的理想选择。
|
|
@ -1,523 +0,0 @@
|
|||||||
# 考试信息API使用文档
|
|
||||||
|
|
||||||
## 接口概述
|
|
||||||
|
|
||||||
`/aiol/aiolExam/getExamInfo` 接口用于获取教师名下的考试信息列表。
|
|
||||||
|
|
||||||
## 接口定义
|
|
||||||
|
|
||||||
### 请求方式
|
|
||||||
- **方法**: GET
|
|
||||||
- **路径**: `/aiol/aiolExam/getExamInfo`
|
|
||||||
- **参数**: `userId` (查询参数)
|
|
||||||
|
|
||||||
### 请求参数
|
|
||||||
|
|
||||||
| 参数名 | 类型 | 必选 | 说明 |
|
|
||||||
|--------|------|------|------|
|
|
||||||
| userId | string | 是 | 教师用户ID |
|
|
||||||
|
|
||||||
### 响应格式
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
"success": boolean,
|
|
||||||
"message": string,
|
|
||||||
"code": number,
|
|
||||||
"result": ExamInfo[],
|
|
||||||
"timestamp": number
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### ExamInfo 数据结构
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface ExamInfo {
|
|
||||||
id: string // 考试ID
|
|
||||||
name: string // 考试名称
|
|
||||||
paperId: string // 试卷ID
|
|
||||||
startTime: string // 开始时间
|
|
||||||
endTime: string // 结束时间
|
|
||||||
totalTime: number // 考试时长(分钟)
|
|
||||||
type: number // 考试类型:0=练习,1=考试
|
|
||||||
status: number // 状态:0=未发布,1=发布中,2=已结束
|
|
||||||
createBy: string // 创建人
|
|
||||||
createTime: string // 创建时间
|
|
||||||
updateBy: string // 更新人
|
|
||||||
updateTime: string // 更新时间
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 使用示例
|
|
||||||
|
|
||||||
### 1. 在API模块中调用
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { ExamApi } from '@/api/modules/exam'
|
|
||||||
|
|
||||||
// 获取教师考试信息
|
|
||||||
const response = await ExamApi.getExamInfo('teacher_user_id')
|
|
||||||
console.log('考试信息:', response.data)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 在Vue组件中使用
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { ExamApi } from '@/api/modules/exam'
|
|
||||||
import { useUserStore } from '@/stores/user'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
setup() {
|
|
||||||
const userStore = useUserStore()
|
|
||||||
|
|
||||||
const loadExamInfo = async () => {
|
|
||||||
if (!userStore.user?.id) {
|
|
||||||
console.error('请先登录')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await ExamApi.getExamInfo(userStore.user.id.toString())
|
|
||||||
return response.data || []
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载考试信息失败:', error)
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
loadExamInfo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 在试卷管理页面中使用
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 在 ExamLibrary.vue 中
|
|
||||||
import { ExamApi } from '@/api/modules/exam'
|
|
||||||
import { useUserStore } from '@/stores/user'
|
|
||||||
import type { ExamInfo } from '@/api/types'
|
|
||||||
|
|
||||||
const userStore = useUserStore()
|
|
||||||
|
|
||||||
const loadExamInfo = async () => {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const currentUser = userStore.user
|
|
||||||
if (!currentUser?.id) {
|
|
||||||
message.error('请先登录')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await ExamApi.getExamInfo(currentUser.id.toString())
|
|
||||||
|
|
||||||
if (response.data && Array.isArray(response.data)) {
|
|
||||||
// 数据映射和显示逻辑
|
|
||||||
const mappedList = response.data.map((item: ExamInfo) => {
|
|
||||||
// 映射逻辑...
|
|
||||||
return {
|
|
||||||
id: item.id,
|
|
||||||
name: item.name,
|
|
||||||
// ... 其他字段映射
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
examData.value = mappedList
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载考试信息失败:', error)
|
|
||||||
message.error('加载考试信息失败')
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 数据映射说明
|
|
||||||
|
|
||||||
### 状态映射
|
|
||||||
```typescript
|
|
||||||
const statusMap: { [key: number]: string } = {
|
|
||||||
0: '未发布',
|
|
||||||
1: '发布中',
|
|
||||||
2: '已结束'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 类型映射
|
|
||||||
```typescript
|
|
||||||
const categoryMap: { [key: number]: string } = {
|
|
||||||
0: '练习',
|
|
||||||
1: '考试'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 难度映射
|
|
||||||
```typescript
|
|
||||||
const difficultyMap: { [key: number]: string } = {
|
|
||||||
0: '易',
|
|
||||||
1: '中',
|
|
||||||
2: '难'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 错误处理
|
|
||||||
|
|
||||||
接口可能返回以下错误:
|
|
||||||
|
|
||||||
1. **401 Unauthorized**: 用户未登录或token过期
|
|
||||||
2. **403 Forbidden**: 没有权限访问
|
|
||||||
3. **500 Internal Server Error**: 服务器内部错误
|
|
||||||
|
|
||||||
建议在调用时添加适当的错误处理:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
try {
|
|
||||||
const response = await ExamApi.getExamInfo(userId)
|
|
||||||
// 处理成功响应
|
|
||||||
} catch (error) {
|
|
||||||
if (error.response?.status === 401) {
|
|
||||||
// 处理认证错误
|
|
||||||
console.error('登录已过期,请重新登录')
|
|
||||||
} else {
|
|
||||||
// 处理其他错误
|
|
||||||
console.error('获取考试信息失败:', error.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. 调用此接口前需要确保用户已登录
|
|
||||||
2. 只有教师用户才能调用此接口
|
|
||||||
3. 返回的考试信息按创建时间倒序排列
|
|
||||||
4. 建议在组件挂载时调用此接口加载数据
|
|
||||||
5. 可以根据需要添加分页、搜索等参数(需要后端支持)
|
|
||||||
|
|
||||||
## 相关文件
|
|
||||||
|
|
||||||
- API实现: `src/api/modules/exam.ts`
|
|
||||||
- 类型定义: `src/api/types.ts`
|
|
||||||
- 使用示例: `src/api/examples/getExamInfo-example.ts`
|
|
||||||
- 页面实现: `src/views/teacher/ExamPages/ExamLibrary.vue`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 创建试卷API使用文档
|
|
||||||
|
|
||||||
## 接口概述
|
|
||||||
|
|
||||||
`POST /aiol/aiolPaper/add` 接口用于创建新的试卷。
|
|
||||||
|
|
||||||
## 接口定义
|
|
||||||
|
|
||||||
### 请求方式
|
|
||||||
- **方法**: POST
|
|
||||||
- **路径**: `/aiol/aiolPaper/add`
|
|
||||||
- **Content-Type**: `application/json`
|
|
||||||
|
|
||||||
### 请求参数
|
|
||||||
|
|
||||||
| 参数名 | 类型 | 必选 | 说明 |
|
|
||||||
|--------|------|------|------|
|
|
||||||
| title | string | 是 | 试卷标题 |
|
|
||||||
| generateMode | number | 否 | 组卷模式:0=固定试卷组,1=随机抽题组卷 |
|
|
||||||
| rules | string | 否 | 组卷规则(随机抽题时使用) |
|
|
||||||
| repoId | string | 否 | 题库ID(随机抽题时使用) |
|
|
||||||
| totalScore | number | 是 | 试卷总分 |
|
|
||||||
| passScore | number | 否 | 及格分数(默认总分的60%) |
|
|
||||||
| requireReview | number | 否 | 是否需要批阅:0=不需要,1=需要 |
|
|
||||||
|
|
||||||
### 响应格式
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
"success": boolean,
|
|
||||||
"message": string,
|
|
||||||
"code": number,
|
|
||||||
"result": string, // 试卷ID
|
|
||||||
"timestamp": number
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 使用示例
|
|
||||||
|
|
||||||
### 1. 在API模块中调用
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { ExamApi } from '@/api/modules/exam'
|
|
||||||
|
|
||||||
// 创建固定试卷组
|
|
||||||
const response = await ExamApi.createExamPaper({
|
|
||||||
title: '数学期末考试试卷',
|
|
||||||
generateMode: 0,
|
|
||||||
totalScore: 100,
|
|
||||||
passScore: 60,
|
|
||||||
requireReview: 0
|
|
||||||
})
|
|
||||||
console.log('试卷ID:', response.data)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 在Vue组件中使用
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 在 AddExam.vue 中
|
|
||||||
const saveExam = async () => {
|
|
||||||
try {
|
|
||||||
const apiData = {
|
|
||||||
title: examForm.title,
|
|
||||||
generateMode: examForm.type === 1 ? 0 : 1,
|
|
||||||
rules: '',
|
|
||||||
repoId: '',
|
|
||||||
totalScore: examForm.totalScore,
|
|
||||||
passScore: examForm.passScore || Math.floor(examForm.totalScore * 0.6),
|
|
||||||
requireReview: examForm.useAIGrading ? 1 : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await ExamApi.createExamPaper(apiData)
|
|
||||||
console.log('创建试卷成功:', response.data)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('创建试卷失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 不同组卷模式示例
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 固定试卷组
|
|
||||||
const fixedPaper = {
|
|
||||||
title: '固定试卷组示例',
|
|
||||||
generateMode: 0,
|
|
||||||
rules: '',
|
|
||||||
repoId: '',
|
|
||||||
totalScore: 100,
|
|
||||||
passScore: 60,
|
|
||||||
requireReview: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// 随机抽题组卷
|
|
||||||
const randomPaper = {
|
|
||||||
title: '随机抽题组卷示例',
|
|
||||||
generateMode: 1,
|
|
||||||
rules: '{"difficulty": [1, 2, 3], "types": [0, 1, 2], "count": 20}',
|
|
||||||
repoId: 'repo_123',
|
|
||||||
totalScore: 100,
|
|
||||||
passScore: 60,
|
|
||||||
requireReview: 1
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 相关文件
|
|
||||||
|
|
||||||
- API实现: `src/api/modules/exam.ts`
|
|
||||||
- 页面实现: `src/views/teacher/ExamPages/AddExam.vue`
|
|
||||||
- 使用示例: `src/api/examples/createPaper-example.ts`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 删除试卷API使用文档
|
|
||||||
|
|
||||||
## 接口概述
|
|
||||||
|
|
||||||
删除试卷相关的API接口包括单个删除和批量删除功能。
|
|
||||||
|
|
||||||
## 接口定义
|
|
||||||
|
|
||||||
### 1. 删除单个试卷
|
|
||||||
|
|
||||||
- **方法**: DELETE
|
|
||||||
- **路径**: `/aiol/aiolPaper/delete?id={paperId}`
|
|
||||||
- **参数**: `id` (查询参数) - 试卷ID
|
|
||||||
|
|
||||||
### 2. 批量删除试卷
|
|
||||||
|
|
||||||
- **实现方式**: 循环调用单个删除接口(因为后端可能不支持批量删除接口)
|
|
||||||
- **方法**: 内部调用多个 `DELETE /aiol/aiolPaper/delete?id={paperId}`
|
|
||||||
- **参数**: `ids: string[]` - 试卷ID数组
|
|
||||||
|
|
||||||
### 响应格式
|
|
||||||
|
|
||||||
**单个删除响应**:
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
"success": boolean,
|
|
||||||
"message": string,
|
|
||||||
"code": number,
|
|
||||||
"result": string,
|
|
||||||
"timestamp": number
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**批量删除响应**:
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
"success": boolean,
|
|
||||||
"message": string,
|
|
||||||
"code": number,
|
|
||||||
"result": {
|
|
||||||
"success": number, // 成功删除的数量
|
|
||||||
"failed": number, // 失败删除的数量
|
|
||||||
"total": number, // 总数量
|
|
||||||
"errors": string[] // 错误信息数组
|
|
||||||
},
|
|
||||||
"timestamp": number
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 使用示例
|
|
||||||
|
|
||||||
### 1. 删除单个试卷
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { ExamApi } from '@/api/modules/exam'
|
|
||||||
|
|
||||||
// 删除单个试卷
|
|
||||||
const response = await ExamApi.deleteExamPaper('1962379646322384897')
|
|
||||||
console.log('删除结果:', response.data)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 批量删除试卷
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 批量删除试卷
|
|
||||||
const paperIds = ['1962379646322384897', '1966450638717292545']
|
|
||||||
const response = await ExamApi.batchDeleteExamPapers(paperIds)
|
|
||||||
console.log('批量删除结果:', response.data)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 在Vue组件中使用(使用 Naive UI 对话框组件)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 在 ExamLibrary.vue 中
|
|
||||||
import { useDialog, useMessage } from 'naive-ui'
|
|
||||||
|
|
||||||
const dialog = useDialog()
|
|
||||||
const message = useMessage()
|
|
||||||
|
|
||||||
const handleDeletePaper = async (row: any) => {
|
|
||||||
try {
|
|
||||||
// 使用 Naive UI 对话框组件
|
|
||||||
dialog.warning({
|
|
||||||
title: '确认删除',
|
|
||||||
content: `确定要删除试卷"${row.name}"吗?此操作不可撤销。`,
|
|
||||||
positiveText: '确定删除',
|
|
||||||
negativeText: '取消',
|
|
||||||
onPositiveClick: async () => {
|
|
||||||
try {
|
|
||||||
// 调用删除API
|
|
||||||
const response = await ExamApi.deleteExamPaper(row.id)
|
|
||||||
|
|
||||||
// 显示成功消息
|
|
||||||
message.success('试卷删除成功!')
|
|
||||||
|
|
||||||
// 重新加载试卷列表
|
|
||||||
await loadExamPaperList()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('删除试卷失败:', error)
|
|
||||||
message.error('删除试卷失败,请重试')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('删除试卷失败:', error)
|
|
||||||
message.error('删除试卷失败,请重试')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量删除示例
|
|
||||||
const handleBatchDelete = async () => {
|
|
||||||
if (checkedRowKeys.value.length === 0) {
|
|
||||||
message.warning('请先选择要删除的试卷')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 使用 Naive UI 对话框组件
|
|
||||||
dialog.error({
|
|
||||||
title: '确认批量删除',
|
|
||||||
content: `确定要删除选中的 ${checkedRowKeys.value.length} 个试卷吗?此操作不可撤销。`,
|
|
||||||
positiveText: '确定删除',
|
|
||||||
negativeText: '取消',
|
|
||||||
onPositiveClick: async () => {
|
|
||||||
try {
|
|
||||||
// 调用批量删除API
|
|
||||||
const response = await ExamApi.batchDeleteExamPapers(checkedRowKeys.value as string[])
|
|
||||||
|
|
||||||
// 显示成功消息
|
|
||||||
message.success(`成功删除 ${checkedRowKeys.value.length} 个试卷!`)
|
|
||||||
|
|
||||||
// 清空选中状态
|
|
||||||
checkedRowKeys.value = []
|
|
||||||
|
|
||||||
// 重新加载试卷列表
|
|
||||||
await loadExamPaperList()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('批量删除试卷失败:', error)
|
|
||||||
message.error('批量删除试卷失败,请重试')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('批量删除试卷失败:', error)
|
|
||||||
message.error('批量删除试卷失败,请重试')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 错误处理
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const deletePaperWithErrorHandling = async (paperId: string) => {
|
|
||||||
try {
|
|
||||||
const response = await ExamApi.deleteExamPaper(paperId)
|
|
||||||
|
|
||||||
if (response.data === 'success') {
|
|
||||||
return { success: true, message: '删除成功' }
|
|
||||||
} else {
|
|
||||||
return { success: false, message: '删除失败,请重试' }
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.response?.status === 404) {
|
|
||||||
return { success: false, message: '试卷不存在' }
|
|
||||||
} else if (error.response?.status === 403) {
|
|
||||||
return { success: false, message: '没有权限删除此试卷' }
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: false, message: '删除失败,请检查网络连接' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 功能特性
|
|
||||||
|
|
||||||
### 单个删除
|
|
||||||
- 支持删除单个试卷
|
|
||||||
- 使用 Naive UI 警告对话框组件
|
|
||||||
- 删除成功后自动刷新列表
|
|
||||||
- 完整的错误处理
|
|
||||||
|
|
||||||
### 批量删除
|
|
||||||
- 支持同时删除多个试卷
|
|
||||||
- 循环调用单个删除接口(避免后端接口不存在的问题)
|
|
||||||
- 逐个删除,避免对服务器造成过大压力
|
|
||||||
- 详细的删除结果反馈(成功/失败数量)
|
|
||||||
- 使用 Naive UI 错误对话框组件(更醒目的警告)
|
|
||||||
- 删除后清空选中状态
|
|
||||||
|
|
||||||
### 用户体验
|
|
||||||
- **美观的确认对话框**: 使用 Naive UI 组件,样式统一美观
|
|
||||||
- **不同类型的对话框**: 单个删除使用 warning,批量删除使用 error
|
|
||||||
- **成功/失败消息提示**: 使用 Naive UI 的 message 组件
|
|
||||||
- **按钮状态管理**: 批量删除按钮在未选中时禁用
|
|
||||||
- **实时更新选中数量显示**: 动态显示选中的试卷数量
|
|
||||||
- **异步操作处理**: 在对话框确认后才执行删除操作
|
|
||||||
|
|
||||||
## 相关文件
|
|
||||||
|
|
||||||
- API实现: `src/api/modules/exam.ts`
|
|
||||||
- 页面实现: `src/views/teacher/ExamPages/ExamLibrary.vue`
|
|
||||||
- 使用示例: `src/api/examples/deletePaper-example.ts`
|
|
@ -1,148 +0,0 @@
|
|||||||
# AI模型字典API调试指南
|
|
||||||
|
|
||||||
## 问题描述
|
|
||||||
|
|
||||||
在调用AI模型字典API时遇到错误:
|
|
||||||
```
|
|
||||||
Sign签名校验失败,时间戳为空!
|
|
||||||
```
|
|
||||||
|
|
||||||
## 已实施的修复措施
|
|
||||||
|
|
||||||
### 1. **添加时间戳参数到URL**
|
|
||||||
```typescript
|
|
||||||
// 修改前
|
|
||||||
return request.get('/sys/dict/getDictItems/airag_model%20where%20model_type%20=%20\'LLM\',name,id')
|
|
||||||
|
|
||||||
// 修改后
|
|
||||||
const timestamp = Date.now()
|
|
||||||
return request.get(`/sys/dict/getDictItems/airag_model%20where%20model_type%20=%20'LLM',name,id?_t=${timestamp}`)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. **增强请求头时间戳**
|
|
||||||
```typescript
|
|
||||||
// 添加多种时间戳格式
|
|
||||||
const timestamp = Date.now().toString()
|
|
||||||
config.headers['X-Request-Time'] = timestamp
|
|
||||||
config.headers['timestamp'] = timestamp
|
|
||||||
config.headers['X-Timestamp'] = timestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. **增强错误调试**
|
|
||||||
- 在AiAppSetting.vue中添加详细的调试日志
|
|
||||||
- 在AiModelTest.vue中添加直接请求测试功能
|
|
||||||
- 显示完整的错误信息和响应数据
|
|
||||||
|
|
||||||
## 测试步骤
|
|
||||||
|
|
||||||
### 1. 访问测试页面
|
|
||||||
```
|
|
||||||
http://localhost:5173/ai-model-test
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 检查登录状态
|
|
||||||
- 确保用户已登录并有有效的token
|
|
||||||
- 在测试页面中查看"用户Token"状态
|
|
||||||
|
|
||||||
### 3. 测试API调用
|
|
||||||
1. 点击"加载AI模型列表"按钮(使用封装的API)
|
|
||||||
2. 点击"直接请求测试"按钮(使用原生axios)
|
|
||||||
3. 查看控制台日志和响应数据
|
|
||||||
|
|
||||||
### 4. 检查AI应用设置
|
|
||||||
1. 访问 `/ai/app` 页面
|
|
||||||
2. 点击任意AI应用卡片
|
|
||||||
3. 切换到"模型配置"选项卡
|
|
||||||
4. 查看控制台日志
|
|
||||||
|
|
||||||
## 调试信息
|
|
||||||
|
|
||||||
### 请求信息
|
|
||||||
- **URL**: `/sys/dict/getDictItems/airag_model%20where%20model_type%20=%20'LLM',name,id?_t={timestamp}`
|
|
||||||
- **方法**: GET
|
|
||||||
- **Base URL**: `/jeecgboot` (或环境变量配置)
|
|
||||||
|
|
||||||
### 请求头
|
|
||||||
```javascript
|
|
||||||
{
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Access-Token': '{user_token}',
|
|
||||||
'X-Request-Time': '{timestamp}',
|
|
||||||
'timestamp': '{timestamp}',
|
|
||||||
'X-Timestamp': '{timestamp}'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 预期响应格式
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"message": "",
|
|
||||||
"code": 0,
|
|
||||||
"result": [
|
|
||||||
{
|
|
||||||
"value": "1890232564262739969",
|
|
||||||
"text": "OpenAI",
|
|
||||||
"color": null,
|
|
||||||
"jsonObject": null,
|
|
||||||
"label": "OpenAI",
|
|
||||||
"title": "OpenAI"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"timestamp": 1759136645858
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 可能的解决方案
|
|
||||||
|
|
||||||
### 1. **检查后端签名算法**
|
|
||||||
如果问题仍然存在,可能需要:
|
|
||||||
- 检查后端期望的签名算法
|
|
||||||
- 添加必要的签名参数
|
|
||||||
- 确认时间戳格式要求
|
|
||||||
|
|
||||||
### 2. **检查接口权限**
|
|
||||||
- 确认该接口是否需要特定的用户权限
|
|
||||||
- 检查token是否有效
|
|
||||||
- 验证用户角色权限
|
|
||||||
|
|
||||||
### 3. **检查接口路径**
|
|
||||||
- 确认接口路径是否正确
|
|
||||||
- 检查URL编码是否正确
|
|
||||||
- 验证查询参数格式
|
|
||||||
|
|
||||||
## 备用方案
|
|
||||||
|
|
||||||
如果API调用仍然失败,系统会自动使用备用数据:
|
|
||||||
```typescript
|
|
||||||
modelOptions.value = [
|
|
||||||
{ label: 'GPT-3.5 Turbo', value: 'gpt-3.5-turbo' },
|
|
||||||
{ label: 'GPT-4', value: 'gpt-4' },
|
|
||||||
{ label: 'Claude-3', value: 'claude-3' }
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
## 下一步调试
|
|
||||||
|
|
||||||
1. **查看网络请求**
|
|
||||||
- 打开浏览器开发者工具
|
|
||||||
- 查看Network选项卡
|
|
||||||
- 检查实际发送的请求和响应
|
|
||||||
|
|
||||||
2. **检查后端日志**
|
|
||||||
- 查看后端服务器日志
|
|
||||||
- 确认请求是否到达后端
|
|
||||||
- 检查签名验证逻辑
|
|
||||||
|
|
||||||
3. **联系后端开发**
|
|
||||||
- 确认接口的正确调用方式
|
|
||||||
- 获取签名算法详细信息
|
|
||||||
- 确认必需的请求参数
|
|
||||||
|
|
||||||
## 文件修改记录
|
|
||||||
|
|
||||||
- `src/api/modules/system.ts`: 添加字典API和时间戳参数
|
|
||||||
- `src/api/request.ts`: 增强时间戳请求头
|
|
||||||
- `src/views/Ai/component/AiAppSetting.vue`: 添加详细调试日志
|
|
||||||
- `src/views/AiModelTest.vue`: 创建专门的测试页面
|
|
||||||
- `src/router/index.ts`: 添加测试页面路由
|
|
@ -1,234 +0,0 @@
|
|||||||
# AI模型字典API集成
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
本文档说明如何在AI应用设置弹窗中集成真实的AI模型字典API,替换原有的模拟数据。
|
|
||||||
|
|
||||||
## API接口信息
|
|
||||||
|
|
||||||
### 获取AI模型字典
|
|
||||||
- **接口地址**: `/sys/dict/getDictItems/airag_model%20where%20model_type%20=%20'LLM',name,id`
|
|
||||||
- **请求方法**: GET
|
|
||||||
- **接口说明**: 获取LLM类型的AI模型字典数据
|
|
||||||
|
|
||||||
### 响应格式
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"message": "",
|
|
||||||
"code": 0,
|
|
||||||
"result": [
|
|
||||||
{
|
|
||||||
"value": "1890232564262739969",
|
|
||||||
"text": "OpenAI",
|
|
||||||
"color": null,
|
|
||||||
"jsonObject": null,
|
|
||||||
"label": "OpenAI",
|
|
||||||
"title": "OpenAI"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "1897481367743143938",
|
|
||||||
"text": "deepseek",
|
|
||||||
"color": null,
|
|
||||||
"jsonObject": null,
|
|
||||||
"label": "deepseek",
|
|
||||||
"title": "deepseek"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "1897883052995006466",
|
|
||||||
"text": "智谱",
|
|
||||||
"color": null,
|
|
||||||
"jsonObject": null,
|
|
||||||
"label": "智谱",
|
|
||||||
"title": "智谱"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "1970031008335876097",
|
|
||||||
"text": "测试",
|
|
||||||
"color": null,
|
|
||||||
"jsonObject": null,
|
|
||||||
"label": "测试",
|
|
||||||
"title": "测试"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"timestamp": 1759136645858
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 实现的功能
|
|
||||||
|
|
||||||
### 1. **API模块扩展** (`src/api/modules/system.ts`)
|
|
||||||
|
|
||||||
添加了字典相关的API接口:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 字典项接口
|
|
||||||
export interface DictItem {
|
|
||||||
value: string
|
|
||||||
text: string
|
|
||||||
color: string | null
|
|
||||||
jsonObject: any | null
|
|
||||||
label: string
|
|
||||||
title: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SystemApi = {
|
|
||||||
// 获取字典项
|
|
||||||
getDictItems(dictCode: string, params?: string): Promise<ApiResponse<DictItem[]>>
|
|
||||||
|
|
||||||
// 获取AI模型字典项(LLM类型)
|
|
||||||
getAiModelDict(): Promise<ApiResponse<DictItem[]>>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. **AI应用设置组件更新** (`src/views/Ai/component/AiAppSetting.vue`)
|
|
||||||
|
|
||||||
修改了 `loadModelOptions` 函数:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 加载模型选项
|
|
||||||
const loadModelOptions = async () => {
|
|
||||||
loadingModels.value = true
|
|
||||||
try {
|
|
||||||
const response = await SystemApi.getAiModelDict()
|
|
||||||
if (response.data.code === 200 || response.data.code === 0) {
|
|
||||||
modelOptions.value = response.data.data.map(item => ({
|
|
||||||
label: item.text,
|
|
||||||
value: item.value
|
|
||||||
}))
|
|
||||||
console.log('✅ AI模型列表加载成功:', modelOptions.value)
|
|
||||||
} else {
|
|
||||||
throw new Error(response.data.message || '获取模型列表失败')
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('❌ 加载模型列表失败:', error)
|
|
||||||
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' }
|
|
||||||
]
|
|
||||||
} finally {
|
|
||||||
loadingModels.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. **测试页面** (`src/views/AiModelTest.vue`)
|
|
||||||
|
|
||||||
创建了专门的测试页面来验证API调用:
|
|
||||||
- 实时加载AI模型数据
|
|
||||||
- 显示API响应状态
|
|
||||||
- 测试选择器功能
|
|
||||||
- 查看原始数据
|
|
||||||
|
|
||||||
## 使用流程
|
|
||||||
|
|
||||||
### 1. 点击AI应用卡片
|
|
||||||
在AI应用列表页面,点击任意应用卡片:
|
|
||||||
```vue
|
|
||||||
<n-card
|
|
||||||
class="app-card"
|
|
||||||
hoverable
|
|
||||||
@click="handleEditApp(item)"
|
|
||||||
>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 打开设置弹窗
|
|
||||||
系统会调用 `handleEditApp` 函数,打开应用设置弹窗:
|
|
||||||
```typescript
|
|
||||||
const handleEditApp = (app: AiApp) => {
|
|
||||||
isEditMode.value = true
|
|
||||||
currentApp.value = { ...app }
|
|
||||||
showSettingModal.value = true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 自动加载模型数据
|
|
||||||
弹窗打开时,会自动调用 `loadModelOptions()` 函数加载AI模型数据:
|
|
||||||
```typescript
|
|
||||||
onMounted(() => {
|
|
||||||
loadModelOptions() // 自动加载模型选项
|
|
||||||
loadFlowOptions()
|
|
||||||
loadSelectedKnowledges()
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 显示模型选择器
|
|
||||||
在"模型配置"选项卡中,用户可以看到从后端加载的真实AI模型列表:
|
|
||||||
- OpenAI
|
|
||||||
- deepseek
|
|
||||||
- 智谱
|
|
||||||
- 测试
|
|
||||||
|
|
||||||
## 测试方法
|
|
||||||
|
|
||||||
### 1. 访问测试页面
|
|
||||||
```
|
|
||||||
http://localhost:5173/ai-model-test
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 测试功能
|
|
||||||
- 点击"加载AI模型列表"按钮
|
|
||||||
- 查看加载状态和结果
|
|
||||||
- 测试选择器功能
|
|
||||||
- 查看原始API响应数据
|
|
||||||
|
|
||||||
### 3. 验证AI应用设置
|
|
||||||
- 访问 `/ai/app` 页面
|
|
||||||
- 点击任意AI应用卡片
|
|
||||||
- 在弹窗中切换到"模型配置"选项卡
|
|
||||||
- 验证AI模型下拉选择器是否显示真实数据
|
|
||||||
|
|
||||||
## 错误处理
|
|
||||||
|
|
||||||
系统包含完整的错误处理机制:
|
|
||||||
|
|
||||||
1. **网络请求失败**: 显示错误消息并使用备用数据
|
|
||||||
2. **API响应错误**: 解析错误信息并提示用户
|
|
||||||
3. **数据格式错误**: 容错处理,确保界面正常显示
|
|
||||||
4. **加载状态**: 显示加载指示器,提升用户体验
|
|
||||||
|
|
||||||
## 数据映射
|
|
||||||
|
|
||||||
API返回的字典项会被映射为选择器选项:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// API数据格式
|
|
||||||
{
|
|
||||||
"value": "1890232564262739969",
|
|
||||||
"text": "OpenAI",
|
|
||||||
"label": "OpenAI",
|
|
||||||
"title": "OpenAI"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 映射为选择器选项
|
|
||||||
{
|
|
||||||
label: "OpenAI", // 显示文本
|
|
||||||
value: "1890232564262739969" // 选择值
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 扩展说明
|
|
||||||
|
|
||||||
### 添加其他字典类型
|
|
||||||
可以在 `SystemApi` 中添加其他字典类型的获取方法:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 获取其他类型的字典
|
|
||||||
getOtherDict(): Promise<ApiResponse<DictItem[]>> {
|
|
||||||
return request.get('/sys/dict/getDictItems/other_dict_code')
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 自定义字典查询
|
|
||||||
使用通用的 `getDictItems` 方法:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 自定义查询条件
|
|
||||||
const response = await SystemApi.getDictItems('dict_code', 'custom_params')
|
|
||||||
```
|
|
||||||
|
|
||||||
这样就完成了AI模型字典API的集成,用户在AI应用设置弹窗中可以看到真实的AI模型数据了!
|
|
@ -1,197 +0,0 @@
|
|||||||
# 动态菜单路由系统
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
本系统实现了基于后端接口的动态菜单路由加载功能,支持从后端获取菜单配置并自动生成前端路由。
|
|
||||||
|
|
||||||
## 接口说明
|
|
||||||
|
|
||||||
### 获取首页菜单
|
|
||||||
- **接口**: `/aiol/aiolMenu/getIndexMenus`
|
|
||||||
- **方法**: GET
|
|
||||||
- **返回格式**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"message": "",
|
|
||||||
"code": 200,
|
|
||||||
"result": [
|
|
||||||
{
|
|
||||||
"id": "1",
|
|
||||||
"name": "首页",
|
|
||||||
"path": "/",
|
|
||||||
"type": "index",
|
|
||||||
"icon": null,
|
|
||||||
"parentId": null,
|
|
||||||
"sortOrder": 99,
|
|
||||||
"izVisible": 1,
|
|
||||||
"permissionKey": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"timestamp": 1759134778382
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 获取学生菜单
|
|
||||||
- **接口**: `/aiol/aiolMenu/getStudentMenus`
|
|
||||||
- **方法**: GET
|
|
||||||
- **返回格式**: 同上,type为"student"
|
|
||||||
|
|
||||||
## 核心文件
|
|
||||||
|
|
||||||
### 1. API模块 (`src/api/modules/menu.ts`)
|
|
||||||
```typescript
|
|
||||||
export interface MenuItem {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
path: string
|
|
||||||
type: string
|
|
||||||
icon: string | null
|
|
||||||
parentId: string | null
|
|
||||||
sortOrder: number
|
|
||||||
izVisible: number
|
|
||||||
permissionKey: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MenuApi {
|
|
||||||
static async getIndexMenus(): Promise<ApiResponse<MenuItem[]>>
|
|
||||||
static async getStudentMenus(): Promise<ApiResponse<MenuItem[]>>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 路由工具 (`src/utils/routeUtils.ts`)
|
|
||||||
```typescript
|
|
||||||
// 根据菜单数据生成路由配置
|
|
||||||
export function generateRoutesFromMenus(menus: MenuItem[]): RouteRecordRaw[]
|
|
||||||
|
|
||||||
// 生成导航菜单数据
|
|
||||||
export function generateNavMenus(menus: MenuItem[])
|
|
||||||
|
|
||||||
// 检查路由是否存在于菜单中
|
|
||||||
export function isRouteInMenus(path: string, menus: MenuItem[]): boolean
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 状态管理 (`src/stores/menu.ts`)
|
|
||||||
```typescript
|
|
||||||
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(() => ...)
|
|
||||||
|
|
||||||
// 方法
|
|
||||||
const fetchIndexMenus = async () => { ... }
|
|
||||||
const fetchStudentMenus = async () => { ... }
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 路由配置 (`src/router/index.ts`)
|
|
||||||
在路由守卫中自动加载动态菜单:
|
|
||||||
```typescript
|
|
||||||
router.beforeEach(async (to, from, next) => {
|
|
||||||
// 加载动态菜单路由(仅在首次访问时)
|
|
||||||
if (!isMenuLoaded) {
|
|
||||||
await loadMenuRoutes()
|
|
||||||
}
|
|
||||||
// ...
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## 使用方法
|
|
||||||
|
|
||||||
### 1. 在组件中使用菜单状态
|
|
||||||
```vue
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useMenuStore } from '@/stores/menu'
|
|
||||||
|
|
||||||
const menuStore = useMenuStore()
|
|
||||||
|
|
||||||
// 获取菜单数据
|
|
||||||
await menuStore.fetchIndexMenus()
|
|
||||||
|
|
||||||
// 使用菜单数据
|
|
||||||
console.log(menuStore.navMenus)
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 使用动态导航组件
|
|
||||||
```vue
|
|
||||||
<template>
|
|
||||||
<DynamicNavigation />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import DynamicNavigation from '@/components/layout/DynamicNavigation.vue'
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 检查路由权限
|
|
||||||
```typescript
|
|
||||||
import { useMenuStore } from '@/stores/menu'
|
|
||||||
|
|
||||||
const menuStore = useMenuStore()
|
|
||||||
|
|
||||||
// 检查路由是否可见
|
|
||||||
const isVisible = menuStore.isRouteVisible('/courses', 'index')
|
|
||||||
```
|
|
||||||
|
|
||||||
## 路径映射
|
|
||||||
|
|
||||||
当前支持的路径映射:
|
|
||||||
|
|
||||||
| 菜单名称 | 路径 | 组件 |
|
|
||||||
|---------|------|------|
|
|
||||||
| 首页 | / | Home |
|
|
||||||
| 热门好课 | /courses | Courses |
|
|
||||||
| 专题训练 | /special-training | SpecialTraining |
|
|
||||||
| 师资力量 | /faculty | Faculty |
|
|
||||||
| 精选资源 | /resources | Resources |
|
|
||||||
| 活动 | /activities | Activities |
|
|
||||||
| AI体验 | /ai/app | AiAppList |
|
|
||||||
|
|
||||||
## 测试页面
|
|
||||||
|
|
||||||
访问 `/menu-test` 可以查看动态菜单系统的测试页面,包含:
|
|
||||||
- 菜单状态监控
|
|
||||||
- 菜单数据展示
|
|
||||||
- 路由跳转测试
|
|
||||||
- 动态导航组件演示
|
|
||||||
|
|
||||||
## 响应式设计
|
|
||||||
|
|
||||||
动态导航组件支持响应式设计:
|
|
||||||
- 桌面端:垂直菜单布局
|
|
||||||
- 移动端:水平滚动菜单布局
|
|
||||||
|
|
||||||
## 错误处理
|
|
||||||
|
|
||||||
系统包含完整的错误处理机制:
|
|
||||||
- 网络请求失败重试
|
|
||||||
- 加载状态显示
|
|
||||||
- 错误信息提示
|
|
||||||
- 手动重试功能
|
|
||||||
|
|
||||||
## 扩展说明
|
|
||||||
|
|
||||||
### 添加新的路径映射
|
|
||||||
在 `src/utils/routeUtils.ts` 中的 `componentMap` 添加新的路径映射:
|
|
||||||
```typescript
|
|
||||||
const componentMap: Record<string, () => Promise<any>> = {
|
|
||||||
'/new-path': () => import('@/views/NewComponent.vue'),
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 自定义菜单图标
|
|
||||||
在 `DynamicNavigation.vue` 中的 `iconMap` 添加新的图标映射:
|
|
||||||
```typescript
|
|
||||||
const iconMap: Record<string, any> = {
|
|
||||||
'new-icon': NewIconComponent,
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
@ -2,15 +2,13 @@
|
|||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<!-- <link rel="icon" href="/logo/logo1.png"> -->
|
<link rel="icon" href="/favicon.ico">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>在线学习平台</title>
|
<title>在线学习平台</title>
|
||||||
<meta name="description" content="专业的在线学习平台,提供优质的编程和技术课程">
|
<meta name="description" content="专业的在线学习平台,提供优质的编程和技术课程">
|
||||||
<meta name="keywords" content="在线学习,编程课程,技术培训,Vue.js,React,Node.js">
|
<meta name="keywords" content="在线学习,编程课程,技术培训,Vue.js,React,Node.js">
|
||||||
<!-- CKPlayer CSS -->
|
<!-- CKPlayer CSS -->
|
||||||
<link rel="stylesheet" href="/ckplayer/css/ckplayer.css">
|
<link rel="stylesheet" href="/ckplayer/css/ckplayer.css">
|
||||||
<!-- HLS.js for m3u8 playback in CKPlayer -->
|
|
||||||
<script src="/ckplayer/hls.js/hls.min.js"></script>
|
|
||||||
<!-- CKPlayer JS -->
|
<!-- CKPlayer JS -->
|
||||||
<script src="/ckplayer/js/ckplayer.js"></script>
|
<script src="/ckplayer/js/ckplayer.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
2255
package-lock.json
generated
18
package.json
@ -13,34 +13,18 @@
|
|||||||
"test:ui": "vitest --ui"
|
"test:ui": "vitest --ui"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/sortablejs": "^1.15.8",
|
|
||||||
"@vicons/antd": "^0.13.0",
|
|
||||||
"@vicons/ionicons5": "^0.13.0",
|
"@vicons/ionicons5": "^0.13.0",
|
||||||
"@wangeditor/editor": "^5.1.23",
|
|
||||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
|
||||||
"@wangeditor/plugin-upload-attachment": "^1.1.0",
|
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"ckplayer": "^3.1.2",
|
"ckplayer": "^3.1.2",
|
||||||
"dplayer": "^1.27.1",
|
|
||||||
"echarts": "5.6.0",
|
|
||||||
"marked": "^16.4.0",
|
|
||||||
"naive-ui": "^2.42.0",
|
"naive-ui": "^2.42.0",
|
||||||
"naive-ui-editor": "^1.0.6",
|
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"quill": "^2.0.3",
|
|
||||||
"vue": "^3.5.17",
|
"vue": "^3.5.17",
|
||||||
"vue-echarts": "7.0.3",
|
|
||||||
"vue-i18n": "^9.14.5",
|
"vue-i18n": "^9.14.5",
|
||||||
"vue-quill-editor": "^3.0.6",
|
"vue-router": "^4.5.1"
|
||||||
"vue-router": "^4.5.1",
|
|
||||||
"vuedraggable": "^4.1.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/dplayer": "^1.25.5",
|
|
||||||
"@types/node": "^24.0.15",
|
"@types/node": "^24.0.15",
|
||||||
"@vitejs/plugin-vue": "^6.0.0",
|
"@vitejs/plugin-vue": "^6.0.0",
|
||||||
"less": "^4.4.2",
|
|
||||||
"sass-embedded": "^1.93.2",
|
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^7.0.0",
|
"vite": "^7.0.0",
|
||||||
"vite-plugin-vue-devtools": "^7.7.7",
|
"vite-plugin-vue-devtools": "^7.7.7",
|
||||||
|
916
pnpm-lock.yaml
generated
Before Width: | Height: | Size: 625 B |
Before Width: | Height: | Size: 806 B |
Before Width: | Height: | Size: 821 B |
Before Width: | Height: | Size: 562 B |
Before Width: | Height: | Size: 775 B |
Before Width: | Height: | Size: 585 B |
Before Width: | Height: | Size: 917 B |
Before Width: | Height: | Size: 834 B |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1011 B |
Before Width: | Height: | Size: 908 B After Width: | Height: | Size: 812 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 944 B |
Before Width: | Height: | Size: 844 B |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 182 KiB |
Before Width: | Height: | Size: 175 KiB |
Before Width: | Height: | Size: 368 KiB |
Before Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 16 MiB |
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 295 KiB |
Before Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 4.1 MiB |
Before Width: | Height: | Size: 3.3 MiB |
Before Width: | Height: | Size: 263 KiB |
Before Width: | Height: | Size: 352 KiB |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 176 KiB |
Before Width: | Height: | Size: 447 B |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 442 B |
Before Width: | Height: | Size: 724 B |
Before Width: | Height: | Size: 516 B |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 858 B |
Before Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 252 KiB |
Before Width: | Height: | Size: 547 B |
Before Width: | Height: | Size: 594 B |
Before Width: | Height: | Size: 3.3 MiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 680 B |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 359 B |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 438 B |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 858 B |
Before Width: | Height: | Size: 594 B |
Before Width: | Height: | Size: 547 B |
Before Width: | Height: | Size: 273 KiB After Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 236 KiB After Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 260 KiB After Width: | Height: | Size: 86 KiB |
Before Width: | Height: | Size: 394 KiB After Width: | Height: | Size: 104 KiB |
Before Width: | Height: | Size: 359 KiB After Width: | Height: | Size: 86 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 9.0 KiB |
Before Width: | Height: | Size: 21 KiB |