feat: 集成AI知识库管理系统
- 新增AI知识库管理功能模块 - 实现知识库的增删改查功能 - 实现文档管理(文本/文件/链接) - 实现向量化功能和测试 - 支持卡片式布局展示 - 完整的响应式设计 - 修复所有TypeScript类型错误 - 添加依赖:marked@^16.4.0, less@^4.2.2 - 打包测试通过 主要文件: - src/views/teacher/ai-knowledge-naive-ui/ - AI知识库管理模块 - docs/ - 相关API文档 - package.json - 新增依赖配置
This commit is contained in:
parent
5de4c9596f
commit
5455490811
@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
type: "manual"
|
type: "always_apply"
|
||||||
|
description: "Example description"
|
||||||
---
|
---
|
||||||
|
|
||||||
1、在接下来的每一个步骤当中,请帮我实现对页面的响应式设计
|
1、在接下来的每一个步骤当中,请帮我实现对页面的响应式设计
|
||||||
|
148
docs/ai-model-api-debug.md
Normal file
148
docs/ai-model-api-debug.md
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
# 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`: 添加测试页面路由
|
234
docs/ai-model-dict-api.md
Normal file
234
docs/ai-model-dict-api.md
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
# 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模型数据了!
|
197
docs/dynamic-menu-routes.md
Normal file
197
docs/dynamic-menu-routes.md
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
# 动态菜单路由系统
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本系统实现了基于后端接口的动态菜单路由加载功能,支持从后端获取菜单配置并自动生成前端路由。
|
||||||
|
|
||||||
|
## 接口说明
|
||||||
|
|
||||||
|
### 获取首页菜单
|
||||||
|
- **接口**: `/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,
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
217
package-lock.json
generated
217
package-lock.json
generated
@ -18,6 +18,7 @@
|
|||||||
"ckplayer": "^3.1.2",
|
"ckplayer": "^3.1.2",
|
||||||
"dplayer": "^1.27.1",
|
"dplayer": "^1.27.1",
|
||||||
"echarts": "5.6.0",
|
"echarts": "5.6.0",
|
||||||
|
"marked": "^16.4.0",
|
||||||
"naive-ui": "^2.42.0",
|
"naive-ui": "^2.42.0",
|
||||||
"naive-ui-editor": "^1.0.6",
|
"naive-ui-editor": "^1.0.6",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
@ -33,6 +34,7 @@
|
|||||||
"@types/dplayer": "^1.25.5",
|
"@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",
|
"sass-embedded": "^1.93.2",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^7.0.0",
|
"vite": "^7.0.0",
|
||||||
@ -2909,6 +2911,20 @@
|
|||||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/errno": {
|
||||||
|
"version": "0.1.8",
|
||||||
|
"resolved": "https://registry.npmmirror.com/errno/-/errno-0.1.8.tgz",
|
||||||
|
"integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"prr": "~1.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"errno": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/error-stack-parser-es": {
|
"node_modules/error-stack-parser-es": {
|
||||||
"version": "0.1.5",
|
"version": "0.1.5",
|
||||||
"resolved": "https://registry.npmmirror.com/error-stack-parser-es/-/error-stack-parser-es-0.1.5.tgz",
|
"resolved": "https://registry.npmmirror.com/error-stack-parser-es/-/error-stack-parser-es-0.1.5.tgz",
|
||||||
@ -3474,6 +3490,34 @@
|
|||||||
"@babel/runtime": "^7.12.0"
|
"@babel/runtime": "^7.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/iconv-lite": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/image-size": {
|
||||||
|
"version": "0.5.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/image-size/-/image-size-0.5.5.tgz",
|
||||||
|
"integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"bin": {
|
||||||
|
"image-size": "bin/image-size.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/immer": {
|
"node_modules/immer": {
|
||||||
"version": "9.0.21",
|
"version": "9.0.21",
|
||||||
"resolved": "https://registry.npmmirror.com/immer/-/immer-9.0.21.tgz",
|
"resolved": "https://registry.npmmirror.com/immer/-/immer-9.0.21.tgz",
|
||||||
@ -3760,6 +3804,53 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/less": {
|
||||||
|
"version": "4.4.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/less/-/less-4.4.2.tgz",
|
||||||
|
"integrity": "sha512-j1n1IuTX1VQjIy3tT7cyGbX7nvQOsFLoIqobZv4ttI5axP923gA44zUj6miiA6R5Aoms4sEGVIIcucXUbRI14g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"copy-anything": "^2.0.1",
|
||||||
|
"parse-node-version": "^1.0.1",
|
||||||
|
"tslib": "^2.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"lessc": "bin/lessc"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"errno": "^0.1.1",
|
||||||
|
"graceful-fs": "^4.1.2",
|
||||||
|
"image-size": "~0.5.0",
|
||||||
|
"make-dir": "^2.1.0",
|
||||||
|
"mime": "^1.4.1",
|
||||||
|
"needle": "^3.1.0",
|
||||||
|
"source-map": "~0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/less/node_modules/copy-anything": {
|
||||||
|
"version": "2.0.6",
|
||||||
|
"resolved": "https://registry.npmmirror.com/copy-anything/-/copy-anything-2.0.6.tgz",
|
||||||
|
"integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-what": "^3.14.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/mesqueeb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/less/node_modules/is-what": {
|
||||||
|
"version": "3.14.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/is-what/-/is-what-3.14.1.tgz",
|
||||||
|
"integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash": {
|
"node_modules/lodash": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
|
||||||
@ -3834,6 +3925,44 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/make-dir": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/make-dir/-/make-dir-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"pify": "^4.0.1",
|
||||||
|
"semver": "^5.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/make-dir/node_modules/semver": {
|
||||||
|
"version": "5.7.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/semver/-/semver-5.7.2.tgz",
|
||||||
|
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"optional": true,
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/marked": {
|
||||||
|
"version": "16.4.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/marked/-/marked-16.4.0.tgz",
|
||||||
|
"integrity": "sha512-CTPAcRBq57cn3R8n3hwc2REddc28hjR7RzDXQ+lXLmMJYqn20BaI2cGw6QjgZGIgVfp2Wdfw4aMzgNteQ6qJgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"marked": "bin/marked.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@ -3872,6 +4001,20 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mime": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"bin": {
|
||||||
|
"mime": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mime-db": {
|
"node_modules/mime-db": {
|
||||||
"version": "1.52.0",
|
"version": "1.52.0",
|
||||||
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
|
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
@ -4000,6 +4143,24 @@
|
|||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/needle": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/needle/-/needle-3.3.1.tgz",
|
||||||
|
"integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"iconv-lite": "^0.6.3",
|
||||||
|
"sax": "^1.2.4"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"needle": "bin/needle"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4.4.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/next-tick": {
|
"node_modules/next-tick": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/next-tick/-/next-tick-1.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/next-tick/-/next-tick-1.1.0.tgz",
|
||||||
@ -4123,6 +4284,16 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/parse-node-version": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/parse-node-version/-/parse-node-version-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/path-browserify": {
|
"node_modules/path-browserify": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz",
|
||||||
@ -4172,6 +4343,17 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pify": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/pify/-/pify-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pinia": {
|
"node_modules/pinia": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmmirror.com/pinia/-/pinia-3.0.3.tgz",
|
"resolved": "https://registry.npmmirror.com/pinia/-/pinia-3.0.3.tgz",
|
||||||
@ -4268,6 +4450,14 @@
|
|||||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/prr": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/prr/-/prr-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/quill": {
|
"node_modules/quill": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmmirror.com/quill/-/quill-2.0.3.tgz",
|
"resolved": "https://registry.npmmirror.com/quill/-/quill-2.0.3.tgz",
|
||||||
@ -4401,6 +4591,14 @@
|
|||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/sass": {
|
"node_modules/sass": {
|
||||||
"version": "1.93.2",
|
"version": "1.93.2",
|
||||||
"resolved": "https://registry.npmmirror.com/sass/-/sass-1.93.2.tgz",
|
"resolved": "https://registry.npmmirror.com/sass/-/sass-1.93.2.tgz",
|
||||||
@ -4772,6 +4970,14 @@
|
|||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sax": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/sax/-/sax-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/scroll-into-view-if-needed": {
|
"node_modules/scroll-into-view-if-needed": {
|
||||||
"version": "2.2.31",
|
"version": "2.2.31",
|
||||||
"resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz",
|
"resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz",
|
||||||
@ -4918,6 +5124,17 @@
|
|||||||
"integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==",
|
"integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/source-map": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
|
||||||
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
"ckplayer": "^3.1.2",
|
"ckplayer": "^3.1.2",
|
||||||
"dplayer": "^1.27.1",
|
"dplayer": "^1.27.1",
|
||||||
"echarts": "5.6.0",
|
"echarts": "5.6.0",
|
||||||
|
"marked": "^16.4.0",
|
||||||
"naive-ui": "^2.42.0",
|
"naive-ui": "^2.42.0",
|
||||||
"naive-ui-editor": "^1.0.6",
|
"naive-ui-editor": "^1.0.6",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
@ -38,6 +39,7 @@
|
|||||||
"@types/dplayer": "^1.25.5",
|
"@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",
|
"sass-embedded": "^1.93.2",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^7.0.0",
|
"vite": "^7.0.0",
|
||||||
|
540
src/views/teacher/ai-knowledge-naive-ui/AiKnowledgeBaseList.vue
Normal file
540
src/views/teacher/ai-knowledge-naive-ui/AiKnowledgeBaseList.vue
Normal file
@ -0,0 +1,540 @@
|
|||||||
|
<template>
|
||||||
|
<div class="knowledge-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">
|
||||||
|
<n-form-item-gi :span="6" label="知识库名称" path="name">
|
||||||
|
<n-input
|
||||||
|
v-model:value="searchForm.name"
|
||||||
|
placeholder="请输入知识库名称"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
/>
|
||||||
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi :span="6" label="创建人" path="createBy">
|
||||||
|
<n-input
|
||||||
|
v-model:value="searchForm.createBy"
|
||||||
|
placeholder="请输入创建人"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
/>
|
||||||
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi :span="6">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- 知识库卡片区域 -->
|
||||||
|
<n-card class="knowledge-cards-container" :bordered="false">
|
||||||
|
<n-grid :cols="6" :x-gap="20" :y-gap="20" responsive="screen">
|
||||||
|
<!-- 创建知识库卡片 -->
|
||||||
|
<n-grid-item>
|
||||||
|
<n-card
|
||||||
|
class="add-knowledge-card"
|
||||||
|
hoverable
|
||||||
|
@click="handleAddKnowledge"
|
||||||
|
>
|
||||||
|
<div class="add-knowledge-content">
|
||||||
|
<n-icon size="24" class="add-icon">
|
||||||
|
<PlusOutlined />
|
||||||
|
</n-icon>
|
||||||
|
<span class="add-text">创建知识库</span>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
</n-grid-item>
|
||||||
|
|
||||||
|
<!-- 知识库列表卡片 -->
|
||||||
|
<n-grid-item v-for="item in knowledgeList" :key="item.id">
|
||||||
|
<n-card
|
||||||
|
class="knowledge-card"
|
||||||
|
hoverable
|
||||||
|
@click="handleDocClick(item.id)"
|
||||||
|
>
|
||||||
|
<div class="knowledge-header">
|
||||||
|
<div class="header-content">
|
||||||
|
<img class="header-img" src="/images/ai/ai.png" alt="知识库" />
|
||||||
|
<div class="header-text">
|
||||||
|
<div class="knowledge-name">{{ item.name }}</div>
|
||||||
|
<div class="knowledge-desc">{{ item.descr || '暂无描述' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<n-dropdown
|
||||||
|
trigger="click"
|
||||||
|
:options="getDropdownOptions(item)"
|
||||||
|
@select="(key: string) => handleDropdownSelect(key, item)"
|
||||||
|
>
|
||||||
|
<n-button quaternary circle size="small">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon><MoreOutlined /></n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</n-dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="knowledge-footer">
|
||||||
|
<div class="knowledge-meta">
|
||||||
|
<n-tag size="small" type="info">
|
||||||
|
文档: {{ item.docCount || 0 }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag size="small" type="success">
|
||||||
|
向量: {{ item.vectorCount || 0 }}
|
||||||
|
</n-tag>
|
||||||
|
</div>
|
||||||
|
<div class="knowledge-creator">
|
||||||
|
创建者: {{ item.createBy_dictText || item.createBy }}
|
||||||
|
</div>
|
||||||
|
<div class="knowledge-time">
|
||||||
|
{{ formatDate(item.createTime || '') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
</n-grid-item>
|
||||||
|
</n-grid>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container" v-if="pagination.total > 0">
|
||||||
|
<n-pagination
|
||||||
|
v-model:page="pagination.page"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
:page-sizes="[10, 20, 30, 50]"
|
||||||
|
show-size-picker
|
||||||
|
show-quick-jumper
|
||||||
|
:show-total="(total: number) => `共 ${total} 条`"
|
||||||
|
@update:page="handlePageChange"
|
||||||
|
@update:page-size="handlePageSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 知识库表单弹窗 -->
|
||||||
|
<KnowledgeBaseModal
|
||||||
|
v-model:show="showKnowledgeModal"
|
||||||
|
:knowledge-data="currentKnowledge"
|
||||||
|
:is-edit="isEditMode"
|
||||||
|
@success="handleModalSuccess"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 知识库文档弹窗 -->
|
||||||
|
<KnowledgeDocListModal
|
||||||
|
v-model:show="showDocModal"
|
||||||
|
:knowledge-id="currentKnowledgeId"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted, h } from 'vue'
|
||||||
|
import { useMessage, useDialog, NIcon } from 'naive-ui'
|
||||||
|
import {
|
||||||
|
SearchOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
MoreOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
SyncOutlined
|
||||||
|
} from '@vicons/antd'
|
||||||
|
import {
|
||||||
|
getKnowledgeList,
|
||||||
|
deleteKnowledge,
|
||||||
|
rebuildKnowledgeVector
|
||||||
|
} from './api/knowledge'
|
||||||
|
import type {
|
||||||
|
KnowledgeBase,
|
||||||
|
KnowledgeSearchForm,
|
||||||
|
Pagination
|
||||||
|
} from './types/knowledge'
|
||||||
|
import { formatDate } from './utils/common'
|
||||||
|
import KnowledgeBaseModal from './components/KnowledgeBaseModal.vue'
|
||||||
|
import KnowledgeDocListModal from './components/KnowledgeDocListModal.vue'
|
||||||
|
|
||||||
|
// 组件引用
|
||||||
|
const searchFormRef = ref()
|
||||||
|
|
||||||
|
// 消息和对话框
|
||||||
|
const message = useMessage()
|
||||||
|
const dialog = useDialog()
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const knowledgeList = ref<KnowledgeBase[]>([])
|
||||||
|
const searchForm = reactive<KnowledgeSearchForm>({
|
||||||
|
name: '',
|
||||||
|
createBy: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const pagination = reactive<Pagination>({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 弹窗状态
|
||||||
|
const showKnowledgeModal = ref(false)
|
||||||
|
const showDocModal = ref(false)
|
||||||
|
const isEditMode = ref(false)
|
||||||
|
const currentKnowledge = ref<Partial<KnowledgeBase>>({})
|
||||||
|
const currentKnowledgeId = ref('')
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 下拉菜单选项
|
||||||
|
const getDropdownOptions = (_item: KnowledgeBase) => [
|
||||||
|
{
|
||||||
|
label: '查看文档',
|
||||||
|
key: 'view',
|
||||||
|
icon: () => h(NIcon, null, { default: () => h(EyeOutlined) })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '编辑',
|
||||||
|
key: 'edit',
|
||||||
|
icon: () => h(NIcon, null, { default: () => h(EditOutlined) })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '向量化',
|
||||||
|
key: 'rebuild',
|
||||||
|
icon: () => h(NIcon, null, { default: () => h(SyncOutlined) })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '删除',
|
||||||
|
key: 'delete',
|
||||||
|
icon: () => h(NIcon, null, { default: () => h(DeleteOutlined) }),
|
||||||
|
props: {
|
||||||
|
style: 'color: #e74c3c;'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 页面初始化
|
||||||
|
onMounted(() => {
|
||||||
|
loadKnowledgeList()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 加载知识库列表
|
||||||
|
const loadKnowledgeList = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const params = {
|
||||||
|
...searchForm,
|
||||||
|
pageNo: pagination.page,
|
||||||
|
pageSize: pagination.pageSize
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await getKnowledgeList(params)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
knowledgeList.value = response.result.records || []
|
||||||
|
pagination.total = response.result.total || 0
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '获取知识库列表失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('获取知识库列表失败')
|
||||||
|
console.error('Load knowledge list error:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.page = 1
|
||||||
|
loadKnowledgeList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
const handleReset = () => {
|
||||||
|
searchForm.name = ''
|
||||||
|
searchForm.createBy = ''
|
||||||
|
pagination.page = 1
|
||||||
|
loadKnowledgeList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页变化
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
pagination.page = page
|
||||||
|
loadKnowledgeList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePageSizeChange = (pageSize: number) => {
|
||||||
|
pagination.pageSize = pageSize
|
||||||
|
pagination.page = 1
|
||||||
|
loadKnowledgeList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增知识库
|
||||||
|
const handleAddKnowledge = () => {
|
||||||
|
isEditMode.value = false
|
||||||
|
currentKnowledge.value = {}
|
||||||
|
showKnowledgeModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看文档
|
||||||
|
const handleDocClick = (knowledgeId: string) => {
|
||||||
|
currentKnowledgeId.value = knowledgeId
|
||||||
|
showDocModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下拉菜单选择
|
||||||
|
const handleDropdownSelect = (key: string, item: KnowledgeBase) => {
|
||||||
|
switch (key) {
|
||||||
|
case 'view':
|
||||||
|
handleDocClick(item.id)
|
||||||
|
break
|
||||||
|
case 'edit':
|
||||||
|
handleEdit(item)
|
||||||
|
break
|
||||||
|
case 'rebuild':
|
||||||
|
handleRebuild(item)
|
||||||
|
break
|
||||||
|
case 'delete':
|
||||||
|
handleDelete(item)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑知识库
|
||||||
|
const handleEdit = (item: KnowledgeBase) => {
|
||||||
|
isEditMode.value = true
|
||||||
|
currentKnowledge.value = { ...item }
|
||||||
|
showKnowledgeModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除知识库
|
||||||
|
const handleDelete = (item: KnowledgeBase) => {
|
||||||
|
dialog.warning({
|
||||||
|
title: '确认删除',
|
||||||
|
content: `是否删除名称为"${item.name}"的知识库?`,
|
||||||
|
positiveText: '确认',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
try {
|
||||||
|
await deleteKnowledge({ id: item.id })
|
||||||
|
message.success('删除成功')
|
||||||
|
loadKnowledgeList()
|
||||||
|
} catch (error) {
|
||||||
|
message.error('删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 向量化
|
||||||
|
const handleRebuild = (item: KnowledgeBase) => {
|
||||||
|
dialog.info({
|
||||||
|
title: '确认向量化',
|
||||||
|
content: `是否对知识库"${item.name}"进行向量化?`,
|
||||||
|
positiveText: '确认',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
try {
|
||||||
|
const response = await rebuildKnowledgeVector({ knowIds: item.id })
|
||||||
|
if (response.success) {
|
||||||
|
message.success('向量化成功')
|
||||||
|
loadKnowledgeList()
|
||||||
|
} else {
|
||||||
|
message.warning('向量化失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.warning('向量化失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹窗成功回调
|
||||||
|
const handleModalSuccess = () => {
|
||||||
|
loadKnowledgeList()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.knowledge-container {
|
||||||
|
padding: 16px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
min-height: 100vh;
|
||||||
|
|
||||||
|
.search-card {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
.n-form-item {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-cards-container {
|
||||||
|
.n-grid {
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-knowledge-card {
|
||||||
|
height: 200px;
|
||||||
|
border: 2px dashed #d9d9d9;
|
||||||
|
background-color: #fafafa;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #1890ff;
|
||||||
|
background-color: #f0f8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-knowledge-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
color: #666;
|
||||||
|
|
||||||
|
.add-icon {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-text {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-card {
|
||||||
|
height: 200px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.header-img {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin-right: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.knowledge-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-footer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 16px;
|
||||||
|
left: 16px;
|
||||||
|
right: 16px;
|
||||||
|
|
||||||
|
.knowledge-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-creator {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 24px;
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.knowledge-container .knowledge-cards-container .n-grid {
|
||||||
|
grid-template-columns: repeat(4, 1fr) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.knowledge-container .knowledge-cards-container .n-grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.knowledge-container .knowledge-cards-container .n-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.knowledge-container .knowledge-cards-container .n-grid {
|
||||||
|
grid-template-columns: 1fr !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
314
src/views/teacher/ai-knowledge-naive-ui/DEPLOYMENT.md
Normal file
314
src/views/teacher/ai-knowledge-naive-ui/DEPLOYMENT.md
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
# 部署指南
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 1. 环境要求
|
||||||
|
|
||||||
|
- Node.js >= 16.0.0
|
||||||
|
- npm >= 8.0.0 或 yarn >= 1.22.0 或 pnpm >= 7.0.0
|
||||||
|
|
||||||
|
### 2. 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 使用 npm
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 使用 yarn
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
# 使用 pnpm
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 环境配置
|
||||||
|
|
||||||
|
创建 `.env` 文件:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# API基础地址
|
||||||
|
VITE_API_BASE_URL=http://localhost:8080/api
|
||||||
|
|
||||||
|
# 上传文件地址
|
||||||
|
VITE_UPLOAD_URL=http://localhost:8080/api/sys/common/upload
|
||||||
|
|
||||||
|
# 静态资源地址
|
||||||
|
VITE_STATIC_URL=http://localhost:8080/api/sys/common/static
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 开发模式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
访问 http://localhost:3000
|
||||||
|
|
||||||
|
### 5. 生产构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
构建产物在 `dist` 目录中。
|
||||||
|
|
||||||
|
## 集成到现有项目
|
||||||
|
|
||||||
|
### 1. 作为组件库使用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install ai-knowledge-naive-ui
|
||||||
|
```
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<AiKnowledgeBaseList />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { AiKnowledgeBaseList } from 'ai-knowledge-naive-ui'
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 直接复制源码
|
||||||
|
|
||||||
|
将 `ai-knowledge-naive-ui` 目录复制到你的项目中,然后:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<AiKnowledgeBaseList />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import AiKnowledgeBaseList from './ai-knowledge-naive-ui/AiKnowledgeBaseList.vue'
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
### HTTP请求配置
|
||||||
|
|
||||||
|
修改 `utils/http.ts` 中的配置:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const http = new HttpClient({
|
||||||
|
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
|
||||||
|
timeout: 60000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json;charset=UTF-8'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证配置
|
||||||
|
|
||||||
|
如果你的API需要认证,修改 `utils/http.ts` 中的请求拦截器:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 请求拦截器
|
||||||
|
this.instance.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
// 添加token等认证信息
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
// 或者从你的状态管理中获取
|
||||||
|
// const token = useAuthStore().token
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 路由配置
|
||||||
|
|
||||||
|
如果你使用Vue Router,可以这样配置路由:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import AiKnowledgeBaseList from './ai-knowledge-naive-ui/AiKnowledgeBaseList.vue'
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/knowledge',
|
||||||
|
name: 'Knowledge',
|
||||||
|
component: AiKnowledgeBaseList,
|
||||||
|
meta: {
|
||||||
|
title: 'AI知识库管理'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
|
```
|
||||||
|
|
||||||
|
## 主题配置
|
||||||
|
|
||||||
|
### 使用Naive UI主题
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<n-config-provider :theme="theme">
|
||||||
|
<AiKnowledgeBaseList />
|
||||||
|
</n-config-provider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { darkTheme } from 'naive-ui'
|
||||||
|
|
||||||
|
const isDark = ref(false)
|
||||||
|
const theme = computed(() => isDark.value ? darkTheme : null)
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自定义主题
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<n-config-provider :theme-overrides="themeOverrides">
|
||||||
|
<AiKnowledgeBaseList />
|
||||||
|
</n-config-provider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { GlobalThemeOverrides } from 'naive-ui'
|
||||||
|
|
||||||
|
const themeOverrides: GlobalThemeOverrides = {
|
||||||
|
common: {
|
||||||
|
primaryColor: '#1890ff',
|
||||||
|
primaryColorHover: '#40a9ff',
|
||||||
|
primaryColorPressed: '#096dd9'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 权限控制
|
||||||
|
|
||||||
|
### 路由守卫
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
// 检查用户是否有访问知识库的权限
|
||||||
|
const hasPermission = checkPermission('knowledge:view')
|
||||||
|
|
||||||
|
if (to.path.startsWith('/knowledge') && !hasPermission) {
|
||||||
|
next('/403')
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 组件级权限
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div v-if="hasKnowledgePermission">
|
||||||
|
<AiKnowledgeBaseList />
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<n-result status="403" title="无权限访问" description="您没有访问AI知识库的权限" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const hasKnowledgePermission = computed(() =>
|
||||||
|
authStore.hasPermission('knowledge:view')
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 性能优化
|
||||||
|
|
||||||
|
### 懒加载
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 路由懒加载
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/knowledge',
|
||||||
|
name: 'Knowledge',
|
||||||
|
component: () => import('./ai-knowledge-naive-ui/AiKnowledgeBaseList.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 组件懒加载
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
|
const AiKnowledgeBaseList = defineAsyncComponent(
|
||||||
|
() => import('./ai-knowledge-naive-ui/AiKnowledgeBaseList.vue')
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
1. **API请求失败**
|
||||||
|
- 检查 `.env` 文件中的API地址配置
|
||||||
|
- 确认后端服务是否正常运行
|
||||||
|
- 检查网络连接和CORS配置
|
||||||
|
|
||||||
|
2. **组件样式异常**
|
||||||
|
- 确认已正确引入Naive UI的样式
|
||||||
|
- 检查CSS预处理器配置
|
||||||
|
- 确认没有样式冲突
|
||||||
|
|
||||||
|
3. **TypeScript类型错误**
|
||||||
|
- 确认已安装所有必要的类型定义包
|
||||||
|
- 检查tsconfig.json配置
|
||||||
|
- 更新依赖包到最新版本
|
||||||
|
|
||||||
|
4. **文件上传失败**
|
||||||
|
- 检查上传接口地址配置
|
||||||
|
- 确认文件大小和类型限制
|
||||||
|
- 检查服务器上传配置
|
||||||
|
|
||||||
|
### 调试技巧
|
||||||
|
|
||||||
|
1. 开启开发者工具的网络面板查看API请求
|
||||||
|
2. 使用Vue DevTools检查组件状态
|
||||||
|
3. 查看浏览器控制台的错误信息
|
||||||
|
4. 使用断点调试JavaScript代码
|
||||||
|
|
||||||
|
## 更新升级
|
||||||
|
|
||||||
|
### 版本更新
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查可更新的包
|
||||||
|
npm outdated
|
||||||
|
|
||||||
|
# 更新依赖
|
||||||
|
npm update
|
||||||
|
|
||||||
|
# 更新到最新版本
|
||||||
|
npm install ai-knowledge-naive-ui@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 迁移指南
|
||||||
|
|
||||||
|
当有重大版本更新时,请查看CHANGELOG.md文件了解变更内容和迁移步骤。
|
186
src/views/teacher/ai-knowledge-naive-ui/README.md
Normal file
186
src/views/teacher/ai-knowledge-naive-ui/README.md
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
# AI知识库管理系统 - Naive UI版本
|
||||||
|
|
||||||
|
这是将原有的Ant Design Vue + JeecgBoot项目中的AI知识库管理功能转换为Naive UI + TypeScript + Vue3版本的实现。
|
||||||
|
|
||||||
|
## 📁 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
ai-knowledge-naive-ui/
|
||||||
|
├── AiKnowledgeBaseList.vue # 主页面组件 - 知识库列表
|
||||||
|
├── api/
|
||||||
|
│ └── knowledge.ts # AI知识库相关API
|
||||||
|
├── components/
|
||||||
|
│ ├── KnowledgeBaseModal.vue # 知识库表单组件
|
||||||
|
│ ├── KnowledgeDocListModal.vue # 知识库文档列表组件
|
||||||
|
│ ├── KnowledgeDocTextModal.vue # 文档编辑组件
|
||||||
|
│ └── TextDescModal.vue # 文本详情组件
|
||||||
|
├── types/
|
||||||
|
│ └── knowledge.ts # 类型定义
|
||||||
|
├── utils/
|
||||||
|
│ ├── http.ts # HTTP请求工具
|
||||||
|
│ ├── clipboard.ts # 剪贴板工具
|
||||||
|
│ ├── file.ts # 文件处理工具
|
||||||
|
│ └── common.ts # 通用工具函数
|
||||||
|
└── README.md # 说明文档
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 组件转换对照
|
||||||
|
|
||||||
|
### 原组件 → 新组件
|
||||||
|
|
||||||
|
| 原组件 | 新组件 | 说明 |
|
||||||
|
|--------|--------|------|
|
||||||
|
| `a-card` | `n-card` | 卡片组件 |
|
||||||
|
| `a-form` | `n-form` | 表单组件 |
|
||||||
|
| `a-input` | `n-input` | 输入框组件 |
|
||||||
|
| `a-select` | `n-select` | 选择器组件 |
|
||||||
|
| `a-button` | `n-button` | 按钮组件 |
|
||||||
|
| `a-modal` | `n-modal` | 弹窗组件 |
|
||||||
|
| `a-pagination` | `n-pagination` | 分页组件 |
|
||||||
|
| `a-tag` | `n-tag` | 标签组件 |
|
||||||
|
| `a-tooltip` | `n-tooltip` | 提示组件 |
|
||||||
|
| `a-dropdown` | `n-dropdown` | 下拉菜单组件 |
|
||||||
|
| `a-upload` | `n-upload` | 上传组件 |
|
||||||
|
| `a-table` | `n-data-table` | 表格组件 |
|
||||||
|
| `a-layout` | `n-layout` | 布局组件 |
|
||||||
|
| `a-menu` | `n-menu` | 菜单组件 |
|
||||||
|
|
||||||
|
## 🚀 使用方法
|
||||||
|
|
||||||
|
### 1. 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install naive-ui @vicons/antd marked
|
||||||
|
# 或
|
||||||
|
yarn add naive-ui @vicons/antd marked
|
||||||
|
# 或
|
||||||
|
pnpm add naive-ui @vicons/antd marked
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 配置Naive UI
|
||||||
|
|
||||||
|
在你的主应用中配置Naive UI:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// main.ts
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import naive from 'naive-ui'
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
app.use(naive)
|
||||||
|
app.mount('#app')
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 使用组件
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<AiKnowledgeBaseList />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import AiKnowledgeBaseList from './ai-knowledge-naive-ui/AiKnowledgeBaseList.vue'
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 配置说明
|
||||||
|
|
||||||
|
### HTTP请求配置
|
||||||
|
|
||||||
|
在 `utils/http.ts` 中配置你的API基础地址:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const http = new HttpClient({
|
||||||
|
baseURL: 'http://your-api-domain.com/api', // 修改为你的API地址
|
||||||
|
timeout: 60000
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 环境变量配置
|
||||||
|
|
||||||
|
创建 `.env` 文件:
|
||||||
|
|
||||||
|
```env
|
||||||
|
VITE_API_BASE_URL=http://localhost:8080/api
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 功能特性
|
||||||
|
|
||||||
|
### 知识库管理
|
||||||
|
- ✅ 知识库列表展示(卡片式布局)
|
||||||
|
- ✅ 创建/编辑知识库
|
||||||
|
- ✅ 删除知识库(带确认)
|
||||||
|
- ✅ 知识库向量化
|
||||||
|
- ✅ 搜索和分页
|
||||||
|
|
||||||
|
### 文档管理
|
||||||
|
- ✅ 文档列表展示(表格形式)
|
||||||
|
- ✅ 支持文本、文件、URL三种文档类型
|
||||||
|
- ✅ 文档编辑和删除
|
||||||
|
- ✅ 批量操作(删除、向量化)
|
||||||
|
- ✅ 清空所有文档
|
||||||
|
- ✅ 文档搜索
|
||||||
|
|
||||||
|
### 向量化测试
|
||||||
|
- ✅ 向量化命中测试
|
||||||
|
- ✅ 测试结果展示
|
||||||
|
- ✅ 结果详情查看
|
||||||
|
|
||||||
|
### 文件上传
|
||||||
|
- ✅ 拖拽上传
|
||||||
|
- ✅ 文件类型验证
|
||||||
|
- ✅ 文件大小限制
|
||||||
|
- ✅ 上传进度显示
|
||||||
|
|
||||||
|
## 🎨 样式特性
|
||||||
|
|
||||||
|
- 响应式设计,支持多种屏幕尺寸
|
||||||
|
- 现代化的卡片式布局
|
||||||
|
- 优雅的动画效果
|
||||||
|
- 一致的视觉风格
|
||||||
|
- 深色/浅色主题支持(Naive UI内置)
|
||||||
|
|
||||||
|
## 🔌 API接口
|
||||||
|
|
||||||
|
### 知识库相关
|
||||||
|
- `GET /airag/knowledge/list` - 获取知识库列表
|
||||||
|
- `POST /airag/knowledge/add` - 创建知识库
|
||||||
|
- `PUT /airag/knowledge/edit` - 编辑知识库
|
||||||
|
- `DELETE /airag/knowledge/delete` - 删除知识库
|
||||||
|
- `PUT /airag/knowledge/rebuild` - 知识库向量化
|
||||||
|
|
||||||
|
### 文档相关
|
||||||
|
- `GET /airag/knowledge/doc/list` - 获取文档列表
|
||||||
|
- `POST /airag/knowledge/doc/edit` - 创建/编辑文档
|
||||||
|
- `DELETE /airag/knowledge/doc/deleteBatch` - 批量删除文档
|
||||||
|
- `DELETE /airag/knowledge/doc/deleteAll` - 清空所有文档
|
||||||
|
- `PUT /airag/knowledge/doc/rebuild` - 文档向量化
|
||||||
|
- `POST /airag/knowledge/embedding/hitTest` - 向量化测试
|
||||||
|
|
||||||
|
## 🐛 注意事项
|
||||||
|
|
||||||
|
1. **图标引用**: 使用`@vicons/antd`包中的图标,需要按需引入
|
||||||
|
2. **样式覆盖**: 使用`:deep()`选择器来覆盖组件内部样式
|
||||||
|
3. **类型导入**: 确保正确导入Naive UI的类型定义
|
||||||
|
4. **API地址**: 根据实际情况修改API基础地址
|
||||||
|
5. **权限控制**: 根据需要添加路由守卫和权限验证
|
||||||
|
6. **文件上传**: 需要配置正确的上传接口地址和认证信息
|
||||||
|
|
||||||
|
## 📝 待完善功能
|
||||||
|
|
||||||
|
- [ ] 国际化支持
|
||||||
|
- [ ] 主题切换
|
||||||
|
- [ ] 更多文件类型支持
|
||||||
|
- [ ] 导入导出功能
|
||||||
|
- [ ] 知识库模板功能
|
||||||
|
- [ ] 实时向量化进度显示
|
||||||
|
- [ ] 文档预览功能
|
||||||
|
|
||||||
|
## 🤝 贡献
|
||||||
|
|
||||||
|
欢迎提交Issue和Pull Request来改进这个项目!
|
||||||
|
|
||||||
|
## 📄 许可证
|
||||||
|
|
||||||
|
MIT License
|
193
src/views/teacher/ai-knowledge-naive-ui/api/knowledge.ts
Normal file
193
src/views/teacher/ai-knowledge-naive-ui/api/knowledge.ts
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
import { defHttp } from '../utils/http'
|
||||||
|
import type {
|
||||||
|
KnowledgeBase,
|
||||||
|
KnowledgeDoc,
|
||||||
|
KnowledgeSearchForm,
|
||||||
|
KnowledgeDocSearchForm,
|
||||||
|
EmbeddingHitResult,
|
||||||
|
RebuildVectorParams
|
||||||
|
} from '../types/knowledge'
|
||||||
|
|
||||||
|
// API 端点枚举
|
||||||
|
enum Api {
|
||||||
|
// 知识库管理
|
||||||
|
list = '/airag/knowledge/list',
|
||||||
|
save = '/airag/knowledge/add',
|
||||||
|
delete = '/airag/knowledge/delete',
|
||||||
|
queryById = '/airag/knowledge/queryById',
|
||||||
|
edit = '/airag/knowledge/edit',
|
||||||
|
rebuild = '/airag/knowledge/rebuild',
|
||||||
|
|
||||||
|
// 知识库文档
|
||||||
|
knowledgeDocList = '/airag/knowledge/doc/list',
|
||||||
|
knowledgeEditDoc = '/airag/knowledge/doc/edit',
|
||||||
|
knowledgeDeleteBatchDoc = '/airag/knowledge/doc/deleteBatch',
|
||||||
|
knowledgeDeleteAllDoc = '/airag/knowledge/doc/deleteAll',
|
||||||
|
knowledgeRebuildDoc = '/airag/knowledge/doc/rebuild',
|
||||||
|
knowledgeEmbeddingHitTest = '/airag/knowledge/embedding/hitTest',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询知识库列表
|
||||||
|
* @param params 查询参数
|
||||||
|
*/
|
||||||
|
export const getKnowledgeList = (params: KnowledgeSearchForm & { pageNo?: number; pageSize?: number }) => {
|
||||||
|
return defHttp.get<{
|
||||||
|
records: KnowledgeBase[]
|
||||||
|
total: number
|
||||||
|
size: number
|
||||||
|
current: number
|
||||||
|
}>({
|
||||||
|
url: Api.list,
|
||||||
|
params
|
||||||
|
}, { isTransformResponse: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID查询知识库详情
|
||||||
|
* @param params 查询参数
|
||||||
|
*/
|
||||||
|
export const getKnowledgeById = (params: { id: string }) => {
|
||||||
|
return defHttp.get<KnowledgeBase>({
|
||||||
|
url: Api.queryById,
|
||||||
|
params
|
||||||
|
}, { isTransformResponse: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增知识库
|
||||||
|
* @param params 知识库数据
|
||||||
|
*/
|
||||||
|
export const createKnowledge = (params: Omit<KnowledgeBase, 'id'>) => {
|
||||||
|
return defHttp.post<KnowledgeBase>({
|
||||||
|
url: Api.save,
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑知识库
|
||||||
|
* @param params 知识库数据
|
||||||
|
*/
|
||||||
|
export const updateKnowledge = (params: KnowledgeBase) => {
|
||||||
|
return defHttp.put<KnowledgeBase>({
|
||||||
|
url: Api.edit,
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除知识库
|
||||||
|
* @param params 删除参数
|
||||||
|
*/
|
||||||
|
export const deleteKnowledge = (params: { id: string }) => {
|
||||||
|
return defHttp.delete({
|
||||||
|
url: Api.delete,
|
||||||
|
params
|
||||||
|
}, { joinParamsToUrl: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识库向量化
|
||||||
|
* @param params 向量化参数
|
||||||
|
*/
|
||||||
|
export const rebuildKnowledgeVector = (params: RebuildVectorParams) => {
|
||||||
|
return defHttp.put({
|
||||||
|
url: Api.rebuild,
|
||||||
|
params,
|
||||||
|
timeout: 2 * 60 * 1000
|
||||||
|
}, {
|
||||||
|
joinParamsToUrl: true,
|
||||||
|
isTransformResponse: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询知识库文档列表
|
||||||
|
* @param params 查询参数
|
||||||
|
*/
|
||||||
|
export const getKnowledgeDocList = (params: KnowledgeDocSearchForm & {
|
||||||
|
knowledgeId: string
|
||||||
|
pageNo?: number
|
||||||
|
pageSize?: number
|
||||||
|
}) => {
|
||||||
|
return defHttp.get<{
|
||||||
|
records: KnowledgeDoc[]
|
||||||
|
total: number
|
||||||
|
size: number
|
||||||
|
current: number
|
||||||
|
}>({
|
||||||
|
url: Api.knowledgeDocList,
|
||||||
|
params
|
||||||
|
}, { isTransformResponse: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增/编辑知识库文档
|
||||||
|
* @param params 文档数据
|
||||||
|
*/
|
||||||
|
export const saveKnowledgeDoc = (params: Partial<KnowledgeDoc>) => {
|
||||||
|
return defHttp.post<KnowledgeDoc>({
|
||||||
|
url: Api.knowledgeEditDoc,
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除文档
|
||||||
|
* @param params 删除参数
|
||||||
|
*/
|
||||||
|
export const batchDeleteKnowledgeDocs = (params: { ids: string }) => {
|
||||||
|
return defHttp.delete({
|
||||||
|
url: Api.knowledgeDeleteBatchDoc,
|
||||||
|
params
|
||||||
|
}, { joinParamsToUrl: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有文档
|
||||||
|
* @param knowledgeId 知识库ID
|
||||||
|
*/
|
||||||
|
export const deleteAllKnowledgeDocs = (knowledgeId: string) => {
|
||||||
|
return defHttp.delete({
|
||||||
|
url: Api.knowledgeDeleteAllDoc,
|
||||||
|
params: { knowledgeId }
|
||||||
|
}, { joinParamsToUrl: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文档向量化
|
||||||
|
* @param params 向量化参数
|
||||||
|
*/
|
||||||
|
export const rebuildDocVector = (params: { docIds: string }) => {
|
||||||
|
return defHttp.put({
|
||||||
|
url: Api.knowledgeRebuildDoc,
|
||||||
|
params
|
||||||
|
}, { joinParamsToUrl: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向量化命中测试
|
||||||
|
* @param params 测试参数
|
||||||
|
*/
|
||||||
|
export const embeddingHitTest = (params: {
|
||||||
|
knowledgeId: string
|
||||||
|
query: string
|
||||||
|
topK?: number
|
||||||
|
}) => {
|
||||||
|
return defHttp.post<EmbeddingHitResult[]>({
|
||||||
|
url: Api.knowledgeEmbeddingHitTest,
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量查询知识库
|
||||||
|
* @param params 查询参数
|
||||||
|
*/
|
||||||
|
export const batchQueryKnowledgeById = (params: { ids: string }) => {
|
||||||
|
return defHttp.get<KnowledgeBase[]>({
|
||||||
|
url: '/airag/knowledge/query/batch/byId',
|
||||||
|
params
|
||||||
|
}, { isTransformResponse: false })
|
||||||
|
}
|
@ -0,0 +1,244 @@
|
|||||||
|
<template>
|
||||||
|
<n-modal
|
||||||
|
v-model:show="showModal"
|
||||||
|
preset="card"
|
||||||
|
:title="modalTitle"
|
||||||
|
style="width: 600px"
|
||||||
|
:mask-closable="false"
|
||||||
|
:closable="true"
|
||||||
|
@close="handleCancel"
|
||||||
|
>
|
||||||
|
<template #header-extra>
|
||||||
|
<n-tooltip trigger="hover">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button
|
||||||
|
quaternary
|
||||||
|
circle
|
||||||
|
size="small"
|
||||||
|
@click="openHelpDoc"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon><QuestionCircleOutlined /></n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
AI知识库文档
|
||||||
|
</n-tooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<n-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="formRules"
|
||||||
|
label-placement="left"
|
||||||
|
:label-width="100"
|
||||||
|
require-mark-placement="right-hanging"
|
||||||
|
>
|
||||||
|
<n-form-item label="知识库名称" path="name">
|
||||||
|
<n-input
|
||||||
|
v-model:value="formData.name"
|
||||||
|
placeholder="请输入知识库名称"
|
||||||
|
:maxlength="64"
|
||||||
|
show-count
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item label="知识库描述" path="descr">
|
||||||
|
<n-input
|
||||||
|
v-model:value="formData.descr"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="描述知识库的内容,详尽的描述将帮助AI能深入理解该知识库的内容,能更准确的检索到内容,提高该知识库的命中率。"
|
||||||
|
:maxlength="256"
|
||||||
|
show-count
|
||||||
|
:autosize="{ minRows: 3, maxRows: 6 }"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item label="向量模型" path="embedId">
|
||||||
|
<n-select
|
||||||
|
v-model:value="formData.embedId"
|
||||||
|
placeholder="请选择向量模型"
|
||||||
|
:options="embedModelOptions"
|
||||||
|
:loading="embedModelLoading"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
|
||||||
|
<template #action>
|
||||||
|
<n-space>
|
||||||
|
<n-button @click="handleCancel">取消</n-button>
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
:loading="submitLoading"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
|
{{ isEdit ? '更新' : '创建' }}
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, computed, watch } from 'vue'
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
import { QuestionCircleOutlined } from '@vicons/antd'
|
||||||
|
import type { FormInst, FormRules, SelectOption } from 'naive-ui'
|
||||||
|
import {
|
||||||
|
createKnowledge,
|
||||||
|
updateKnowledge,
|
||||||
|
getKnowledgeById
|
||||||
|
} from '../api/knowledge'
|
||||||
|
import type { KnowledgeBase, KnowledgeFormData } from '../types/knowledge'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
show: boolean
|
||||||
|
knowledgeData?: Partial<KnowledgeBase>
|
||||||
|
isEdit?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:show', value: boolean): void
|
||||||
|
(e: 'success'): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
show: false,
|
||||||
|
isEdit: false,
|
||||||
|
knowledgeData: () => ({})
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
|
// 组件引用
|
||||||
|
const formRef = ref<FormInst>()
|
||||||
|
|
||||||
|
// 消息
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const formData = reactive<KnowledgeFormData>({
|
||||||
|
name: '',
|
||||||
|
descr: '',
|
||||||
|
embedId: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const submitLoading = ref(false)
|
||||||
|
const embedModelLoading = ref(false)
|
||||||
|
const embedModelOptions = ref<SelectOption[]>([])
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const showModal = computed({
|
||||||
|
get: () => props.show,
|
||||||
|
set: (value) => emit('update:show', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const modalTitle = computed(() => props.isEdit ? '编辑知识库' : '创建知识库')
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const formRules: FormRules = {
|
||||||
|
name: [
|
||||||
|
{ required: true, message: '请输入知识库名称', trigger: 'blur' },
|
||||||
|
{ min: 1, max: 64, message: '知识库名称长度应在1-64个字符之间', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
descr: [
|
||||||
|
{ max: 256, message: '描述长度不能超过256个字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
embedId: [
|
||||||
|
{ required: true, message: '请选择向量模型', trigger: 'change' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听弹窗显示状态
|
||||||
|
watch(() => props.show, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
initForm()
|
||||||
|
loadEmbedModels()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 初始化表单
|
||||||
|
const initForm = async () => {
|
||||||
|
if (props.isEdit && props.knowledgeData?.id) {
|
||||||
|
try {
|
||||||
|
const response = await getKnowledgeById({ id: props.knowledgeData.id })
|
||||||
|
if (response.success) {
|
||||||
|
Object.assign(formData, response.result)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('获取知识库详情失败')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 重置表单
|
||||||
|
Object.assign(formData, {
|
||||||
|
name: '',
|
||||||
|
descr: '',
|
||||||
|
embedId: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载向量模型选项
|
||||||
|
const loadEmbedModels = async () => {
|
||||||
|
try {
|
||||||
|
embedModelLoading.value = true
|
||||||
|
// 这里需要调用获取向量模型的API
|
||||||
|
// 暂时使用模拟数据
|
||||||
|
embedModelOptions.value = [
|
||||||
|
{ label: 'text-embedding-ada-002', value: '1' },
|
||||||
|
{ label: 'text-embedding-3-small', value: '2' },
|
||||||
|
{ label: 'text-embedding-3-large', value: '3' }
|
||||||
|
]
|
||||||
|
} catch (error) {
|
||||||
|
message.error('获取向量模型失败')
|
||||||
|
} finally {
|
||||||
|
embedModelLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
await formRef.value?.validate()
|
||||||
|
submitLoading.value = true
|
||||||
|
|
||||||
|
if (props.isEdit) {
|
||||||
|
await updateKnowledge({ ...formData, id: props.knowledgeData?.id! })
|
||||||
|
message.success('更新成功')
|
||||||
|
} else {
|
||||||
|
await createKnowledge(formData)
|
||||||
|
message.success('创建成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
showModal.value = false
|
||||||
|
emit('success')
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
message.error(error.message)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
submitLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消
|
||||||
|
const handleCancel = () => {
|
||||||
|
showModal.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开帮助文档
|
||||||
|
const openHelpDoc = () => {
|
||||||
|
window.open('https://help.jeecg.com/aigc/guide/knowledge', '_blank')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.n-form {
|
||||||
|
.n-form-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,775 @@
|
|||||||
|
<template>
|
||||||
|
<n-modal
|
||||||
|
v-model:show="showModal"
|
||||||
|
preset="card"
|
||||||
|
title="知识库文档管理"
|
||||||
|
style="width: 90vw; height: 80vh"
|
||||||
|
:mask-closable="false"
|
||||||
|
:closable="true"
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
|
<div class="doc-list-container">
|
||||||
|
<n-layout has-sider style="height: 100%">
|
||||||
|
<!-- 左侧菜单 -->
|
||||||
|
<n-layout-sider
|
||||||
|
bordered
|
||||||
|
:width="200"
|
||||||
|
:collapsed-width="64"
|
||||||
|
show-trigger
|
||||||
|
collapse-mode="width"
|
||||||
|
>
|
||||||
|
<n-menu
|
||||||
|
v-model:value="selectedMenuKey"
|
||||||
|
:options="menuOptions"
|
||||||
|
@update:value="handleMenuSelect"
|
||||||
|
/>
|
||||||
|
</n-layout-sider>
|
||||||
|
|
||||||
|
<!-- 右侧内容区域 -->
|
||||||
|
<n-layout-content style="padding: 16px; overflow-y: auto; max-height: calc(80vh - 100px)">
|
||||||
|
<!-- 文档管理 -->
|
||||||
|
<div v-if="selectedMenuKey === 'document'">
|
||||||
|
<!-- 搜索和操作区域 -->
|
||||||
|
<div class="doc-header">
|
||||||
|
<n-space align="center" justify="space-between">
|
||||||
|
<n-space align="center">
|
||||||
|
<n-input
|
||||||
|
v-model:value="searchText"
|
||||||
|
placeholder="请输入文档名称,回车搜索"
|
||||||
|
style="width: 300px"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<n-icon><SearchOutlined /></n-icon>
|
||||||
|
</template>
|
||||||
|
</n-input>
|
||||||
|
|
||||||
|
<n-button type="primary" @click="handleSearch">
|
||||||
|
搜索
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
|
||||||
|
<n-space>
|
||||||
|
<n-button
|
||||||
|
v-if="selectedRows.length > 0"
|
||||||
|
type="error"
|
||||||
|
@click="handleBatchDelete"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon><DeleteOutlined /></n-icon>
|
||||||
|
</template>
|
||||||
|
批量删除 ({{ selectedRows.length }})
|
||||||
|
</n-button>
|
||||||
|
|
||||||
|
<n-button
|
||||||
|
v-if="selectedRows.length > 0"
|
||||||
|
type="info"
|
||||||
|
@click="handleBatchRebuild"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon><SyncOutlined /></n-icon>
|
||||||
|
</template>
|
||||||
|
批量向量化
|
||||||
|
</n-button>
|
||||||
|
|
||||||
|
<n-dropdown
|
||||||
|
trigger="click"
|
||||||
|
:options="addDocOptions"
|
||||||
|
@select="handleAddDoc"
|
||||||
|
>
|
||||||
|
<n-button type="primary">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon><PlusOutlined /></n-icon>
|
||||||
|
</template>
|
||||||
|
添加文档
|
||||||
|
</n-button>
|
||||||
|
</n-dropdown>
|
||||||
|
|
||||||
|
<n-button
|
||||||
|
type="warning"
|
||||||
|
@click="handleClearAllDocs"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon><ClearOutlined /></n-icon>
|
||||||
|
</template>
|
||||||
|
清空文档
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文档列表 - 卡片式布局 -->
|
||||||
|
<n-spin :show="docLoading">
|
||||||
|
<n-empty v-if="docList.length === 0" description="暂无文档" style="margin-top: 60px" />
|
||||||
|
|
||||||
|
<n-grid v-else :cols="5" :x-gap="16" :y-gap="16" style="margin-top: 16px">
|
||||||
|
<n-grid-item v-for="doc in docList" :key="doc.id">
|
||||||
|
<n-card
|
||||||
|
class="doc-card"
|
||||||
|
hoverable
|
||||||
|
:content-style="{ padding: '16px' }"
|
||||||
|
>
|
||||||
|
<!-- 文档图标和名称 -->
|
||||||
|
<div class="doc-card-header">
|
||||||
|
<n-icon size="40" color="#ff6b6b">
|
||||||
|
<FileTextOutlined />
|
||||||
|
</n-icon>
|
||||||
|
<div class="doc-name" :title="doc.title">{{ doc.title }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文档状态 -->
|
||||||
|
<div class="doc-status">
|
||||||
|
<span>状态:</span>
|
||||||
|
<n-tag
|
||||||
|
:type="getStatusType(doc.status)"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ getStatusText(doc.status) }}
|
||||||
|
</n-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="doc-actions">
|
||||||
|
<n-space size="small">
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
quaternary
|
||||||
|
@click="handleEditDoc(doc)"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
type="info"
|
||||||
|
quaternary
|
||||||
|
@click="handleRebuildDoc(doc)"
|
||||||
|
>
|
||||||
|
向量化
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
type="error"
|
||||||
|
quaternary
|
||||||
|
@click="handleDeleteDoc(doc)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
</n-grid-item>
|
||||||
|
</n-grid>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div v-if="docList.length > 0" class="doc-pagination">
|
||||||
|
<n-pagination
|
||||||
|
v-model:page="docPagination.page"
|
||||||
|
v-model:page-size="docPagination.pageSize"
|
||||||
|
:total="docPagination.total"
|
||||||
|
:page-sizes="[10, 20, 30, 50]"
|
||||||
|
show-size-picker
|
||||||
|
show-quick-jumper
|
||||||
|
@update:page="handleDocPageChange"
|
||||||
|
@update:page-size="handleDocPageSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</n-spin>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 向量化测试 -->
|
||||||
|
<div v-else-if="selectedMenuKey === 'test'">
|
||||||
|
<div class="test-container">
|
||||||
|
<n-card title="向量化命中测试" :bordered="false">
|
||||||
|
<n-form
|
||||||
|
ref="testFormRef"
|
||||||
|
:model="testForm"
|
||||||
|
label-placement="top"
|
||||||
|
>
|
||||||
|
<n-form-item label="测试查询" path="query">
|
||||||
|
<n-input
|
||||||
|
v-model:value="testForm.query"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="请输入要测试的查询内容"
|
||||||
|
:autosize="{ minRows: 3, maxRows: 6 }"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item label="返回数量" path="topK">
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="testForm.topK"
|
||||||
|
:min="1"
|
||||||
|
:max="20"
|
||||||
|
placeholder="返回结果数量"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item>
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
:loading="testLoading"
|
||||||
|
@click="handleEmbeddingTest"
|
||||||
|
>
|
||||||
|
开始测试
|
||||||
|
</n-button>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
|
||||||
|
<!-- 测试结果 -->
|
||||||
|
<div v-if="testResults.length > 0" class="test-results">
|
||||||
|
<n-divider title-placement="left">测试结果</n-divider>
|
||||||
|
<n-list>
|
||||||
|
<n-list-item
|
||||||
|
v-for="(result, index) in testResults"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<n-card size="small" hoverable @click="handleViewResult(result)">
|
||||||
|
<template #header>
|
||||||
|
<n-space align="center" justify="space-between">
|
||||||
|
<n-tag type="info" size="small">
|
||||||
|
相似度: {{ result.score.toFixed(4) }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag size="small">
|
||||||
|
{{ result.source }}
|
||||||
|
</n-tag>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
<n-ellipsis :line-clamp="3">
|
||||||
|
{{ result.content }}
|
||||||
|
</n-ellipsis>
|
||||||
|
</n-card>
|
||||||
|
</n-list-item>
|
||||||
|
</n-list>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-layout-content>
|
||||||
|
</n-layout>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文档编辑弹窗 -->
|
||||||
|
<KnowledgeDocTextModal
|
||||||
|
v-model:show="showDocModal"
|
||||||
|
:knowledge-id="knowledgeId"
|
||||||
|
:doc-data="currentDoc"
|
||||||
|
:is-edit="isEditDoc"
|
||||||
|
@success="handleDocModalSuccess"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 文本详情弹窗 -->
|
||||||
|
<TextDescModal
|
||||||
|
v-model:show="showTextModal"
|
||||||
|
:text-data="currentTextData"
|
||||||
|
/>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, computed, watch, h } from 'vue'
|
||||||
|
import { useMessage, useDialog, NTag, NIcon, NButton, NSpin } from 'naive-ui'
|
||||||
|
import type { MenuOption } from 'naive-ui'
|
||||||
|
import {
|
||||||
|
SearchOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
SyncOutlined,
|
||||||
|
ClearOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
UploadOutlined,
|
||||||
|
LinkOutlined,
|
||||||
|
ExperimentOutlined
|
||||||
|
} from '@vicons/antd'
|
||||||
|
import {
|
||||||
|
getKnowledgeDocList,
|
||||||
|
batchDeleteKnowledgeDocs,
|
||||||
|
deleteAllKnowledgeDocs,
|
||||||
|
rebuildDocVector,
|
||||||
|
embeddingHitTest
|
||||||
|
} from '../api/knowledge'
|
||||||
|
import type {
|
||||||
|
KnowledgeDoc,
|
||||||
|
EmbeddingHitResult
|
||||||
|
} from '../types/knowledge'
|
||||||
|
import KnowledgeDocTextModal from './KnowledgeDocTextModal.vue'
|
||||||
|
import TextDescModal from './TextDescModal.vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
show: boolean
|
||||||
|
knowledgeId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:show', value: boolean): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
|
// 组件引用
|
||||||
|
const testFormRef = ref()
|
||||||
|
|
||||||
|
// 消息和对话框
|
||||||
|
const message = useMessage()
|
||||||
|
const dialog = useDialog()
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const docList = ref<KnowledgeDoc[]>([])
|
||||||
|
const searchText = ref('')
|
||||||
|
const docLoading = ref(false)
|
||||||
|
const selectedMenuKey = ref('document')
|
||||||
|
const checkedRowKeys = ref<string[]>([])
|
||||||
|
|
||||||
|
// 测试相关
|
||||||
|
const testForm = reactive({
|
||||||
|
query: '',
|
||||||
|
topK: 5
|
||||||
|
})
|
||||||
|
const testLoading = ref(false)
|
||||||
|
const testResults = ref<EmbeddingHitResult[]>([])
|
||||||
|
|
||||||
|
// 弹窗状态
|
||||||
|
const showDocModal = ref(false)
|
||||||
|
const showTextModal = ref(false)
|
||||||
|
const isEditDoc = ref(false)
|
||||||
|
const currentDoc = ref<Partial<KnowledgeDoc>>({})
|
||||||
|
const currentTextData = ref<any>({})
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const docPagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const tablePagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
showSizePicker: true,
|
||||||
|
pageSizes: [10, 20, 50],
|
||||||
|
onChange: (page: number) => {
|
||||||
|
tablePagination.page = page
|
||||||
|
loadDocList()
|
||||||
|
},
|
||||||
|
onUpdatePageSize: (pageSize: number) => {
|
||||||
|
tablePagination.pageSize = pageSize
|
||||||
|
tablePagination.page = 1
|
||||||
|
loadDocList()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const showModal = computed({
|
||||||
|
get: () => props.show,
|
||||||
|
set: (value) => emit('update:show', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedRows = computed(() =>
|
||||||
|
docList.value.filter(item => checkedRowKeys.value.includes(item.id))
|
||||||
|
)
|
||||||
|
|
||||||
|
// 左侧菜单选项
|
||||||
|
const menuOptions: MenuOption[] = [
|
||||||
|
{
|
||||||
|
label: '文档管理',
|
||||||
|
key: 'document',
|
||||||
|
icon: () => h(NIcon, null, { default: () => h(FileTextOutlined) })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '向量化测试',
|
||||||
|
key: 'test',
|
||||||
|
icon: () => h(NIcon, null, { default: () => h(ExperimentOutlined) })
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 添加文档选项
|
||||||
|
const addDocOptions = [
|
||||||
|
{
|
||||||
|
label: '文本文档',
|
||||||
|
key: 'text',
|
||||||
|
icon: () => h(NIcon, null, { default: () => h(FileTextOutlined) })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '上传文件',
|
||||||
|
key: 'file',
|
||||||
|
icon: () => h(NIcon, null, { default: () => h(UploadOutlined) })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '网页链接',
|
||||||
|
key: 'url',
|
||||||
|
icon: () => h(NIcon, null, { default: () => h(LinkOutlined) })
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 监听弹窗显示状态
|
||||||
|
watch(() => props.show, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
selectedMenuKey.value = 'document'
|
||||||
|
loadDocList()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 加载文档列表
|
||||||
|
const loadDocList = async () => {
|
||||||
|
if (!props.knowledgeId) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
docLoading.value = true
|
||||||
|
const params: any = {
|
||||||
|
pageNo: docPagination.page,
|
||||||
|
pageSize: docPagination.pageSize,
|
||||||
|
knowledgeId: props.knowledgeId,
|
||||||
|
column: 'createTime',
|
||||||
|
order: 'desc'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有在有搜索文本时才添加 title 参数
|
||||||
|
if (searchText.value) {
|
||||||
|
params.title = searchText.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await getKnowledgeDocList(params)
|
||||||
|
|
||||||
|
console.log('📋 请求参数:', params)
|
||||||
|
console.log('📋 API 响应:', response)
|
||||||
|
console.log('📋 响应数据:', response.result)
|
||||||
|
console.log('📋 文档记录:', response.result?.records)
|
||||||
|
console.log('📋 记录数量:', response.result?.records?.length)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
docList.value = response.result.records || []
|
||||||
|
docPagination.total = response.result.total || 0
|
||||||
|
|
||||||
|
console.log('✅ docList.value:', docList.value)
|
||||||
|
console.log('✅ docList 长度:', docList.value.length)
|
||||||
|
console.log('✅ 分页总数:', docPagination.total)
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '获取文档列表失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('获取文档列表失败')
|
||||||
|
console.error('Load doc list error:', error)
|
||||||
|
} finally {
|
||||||
|
docLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
docPagination.page = 1
|
||||||
|
tablePagination.page = 1
|
||||||
|
loadDocList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页变化
|
||||||
|
const handleDocPageChange = (page: number) => {
|
||||||
|
docPagination.page = page
|
||||||
|
loadDocList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDocPageSizeChange = (pageSize: number) => {
|
||||||
|
docPagination.pageSize = pageSize
|
||||||
|
docPagination.page = 1
|
||||||
|
loadDocList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 菜单选择
|
||||||
|
const handleMenuSelect = (key: string) => {
|
||||||
|
selectedMenuKey.value = key
|
||||||
|
if (key === 'document') {
|
||||||
|
loadDocList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加文档
|
||||||
|
const handleAddDoc = (key: string) => {
|
||||||
|
isEditDoc.value = false
|
||||||
|
currentDoc.value = { type: key as any }
|
||||||
|
showDocModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑文档
|
||||||
|
const handleEditDoc = (doc: KnowledgeDoc) => {
|
||||||
|
isEditDoc.value = true
|
||||||
|
currentDoc.value = { ...doc }
|
||||||
|
showDocModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除文档
|
||||||
|
const handleDeleteDoc = (doc: KnowledgeDoc) => {
|
||||||
|
dialog.warning({
|
||||||
|
title: '确认删除',
|
||||||
|
content: `是否删除文档"${doc.title}"?`,
|
||||||
|
positiveText: '确认',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
try {
|
||||||
|
await batchDeleteKnowledgeDocs({ ids: doc.id })
|
||||||
|
message.success('删除成功')
|
||||||
|
loadDocList()
|
||||||
|
} catch (error) {
|
||||||
|
message.error('删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量删除
|
||||||
|
const handleBatchDelete = () => {
|
||||||
|
if (selectedRows.value.length === 0) return
|
||||||
|
|
||||||
|
dialog.warning({
|
||||||
|
title: '确认删除',
|
||||||
|
content: `是否删除选中的 ${selectedRows.value.length} 个文档?`,
|
||||||
|
positiveText: '确认',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
try {
|
||||||
|
const ids = selectedRows.value.map(item => item.id).join(',')
|
||||||
|
await batchDeleteKnowledgeDocs({ ids })
|
||||||
|
message.success('删除成功')
|
||||||
|
checkedRowKeys.value = []
|
||||||
|
loadDocList()
|
||||||
|
} catch (error) {
|
||||||
|
message.error('删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 向量化文档
|
||||||
|
const handleRebuildDoc = (doc: KnowledgeDoc) => {
|
||||||
|
dialog.info({
|
||||||
|
title: '确认向量化',
|
||||||
|
content: `是否对文档"${doc.title}"进行向量化?`,
|
||||||
|
positiveText: '确认',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
try {
|
||||||
|
await rebuildDocVector({ docIds: doc.id })
|
||||||
|
message.success('向量化成功')
|
||||||
|
loadDocList()
|
||||||
|
} catch (error) {
|
||||||
|
message.error('向量化失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量向量化
|
||||||
|
const handleBatchRebuild = () => {
|
||||||
|
if (selectedRows.value.length === 0) return
|
||||||
|
|
||||||
|
dialog.info({
|
||||||
|
title: '确认向量化',
|
||||||
|
content: `是否对选中的 ${selectedRows.value.length} 个文档进行向量化?`,
|
||||||
|
positiveText: '确认',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
try {
|
||||||
|
const ids = selectedRows.value.map(item => item.id).join(',')
|
||||||
|
await rebuildDocVector({ docIds: ids })
|
||||||
|
message.success('向量化成功')
|
||||||
|
checkedRowKeys.value = []
|
||||||
|
loadDocList()
|
||||||
|
} catch (error) {
|
||||||
|
message.error('向量化失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空所有文档
|
||||||
|
const handleClearAllDocs = () => {
|
||||||
|
dialog.error({
|
||||||
|
title: '清空文档',
|
||||||
|
content: '确定要清空所有文档吗?此操作会删除所有已录入的文档,并且不能恢复,请谨慎操作',
|
||||||
|
positiveText: '确认',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
try {
|
||||||
|
await deleteAllKnowledgeDocs(props.knowledgeId)
|
||||||
|
message.success('清空成功')
|
||||||
|
loadDocList()
|
||||||
|
} catch (error) {
|
||||||
|
message.error('清空失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 向量化测试
|
||||||
|
const handleEmbeddingTest = async () => {
|
||||||
|
try {
|
||||||
|
await testFormRef.value?.validate()
|
||||||
|
testLoading.value = true
|
||||||
|
|
||||||
|
const response = await embeddingHitTest({
|
||||||
|
knowledgeId: props.knowledgeId,
|
||||||
|
query: testForm.query,
|
||||||
|
topK: testForm.topK
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
testResults.value = response.result || []
|
||||||
|
if (testResults.value.length === 0) {
|
||||||
|
message.info('未找到匹配的内容')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '测试失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('测试失败')
|
||||||
|
} finally {
|
||||||
|
testLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看测试结果详情
|
||||||
|
const handleViewResult = (result: EmbeddingHitResult) => {
|
||||||
|
currentTextData.value = result
|
||||||
|
showTextModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文档弹窗成功回调
|
||||||
|
const handleDocModalSuccess = () => {
|
||||||
|
loadDocList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态类型
|
||||||
|
const getStatusType = (status?: string): 'warning' | 'success' | 'error' => {
|
||||||
|
const statusMap: Record<string, 'warning' | 'success' | 'error'> = {
|
||||||
|
processing: 'warning',
|
||||||
|
building: 'warning', // 构建中
|
||||||
|
completed: 'success',
|
||||||
|
complete: 'success', // 支持后端返回的 'complete'
|
||||||
|
failed: 'error',
|
||||||
|
error: 'error'
|
||||||
|
}
|
||||||
|
return statusMap[status || 'complete'] || 'success'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态文本
|
||||||
|
const getStatusText = (status?: string): string => {
|
||||||
|
const statusMap: Record<string, string> = {
|
||||||
|
processing: '处理中',
|
||||||
|
building: '构建中', // 构建中
|
||||||
|
completed: '已完成',
|
||||||
|
complete: '已完成', // 支持后端返回的 'complete'
|
||||||
|
failed: '失败',
|
||||||
|
error: '失败'
|
||||||
|
}
|
||||||
|
return statusMap[status || 'complete'] || '已完成'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
const handleClose = () => {
|
||||||
|
showModal.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.doc-list-container {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.doc-header {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-card {
|
||||||
|
height: 180px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-card-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.doc-name {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-actions {
|
||||||
|
margin-top: auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 24px;
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-container {
|
||||||
|
.test-results {
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
.n-list-item {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计
|
||||||
|
@media (max-width: 1600px) {
|
||||||
|
.doc-list-container .n-grid {
|
||||||
|
grid-template-columns: repeat(4, 1fr) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.doc-list-container .n-grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.doc-list-container .n-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.doc-list-container .n-grid {
|
||||||
|
grid-template-columns: 1fr !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,347 @@
|
|||||||
|
<template>
|
||||||
|
<n-modal
|
||||||
|
v-model:show="showModal"
|
||||||
|
preset="card"
|
||||||
|
:title="modalTitle"
|
||||||
|
style="width: 800px"
|
||||||
|
:mask-closable="false"
|
||||||
|
:closable="true"
|
||||||
|
@close="handleCancel"
|
||||||
|
>
|
||||||
|
<n-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="formRules"
|
||||||
|
label-placement="left"
|
||||||
|
:label-width="100"
|
||||||
|
require-mark-placement="right-hanging"
|
||||||
|
>
|
||||||
|
<n-form-item label="文档标题" path="title">
|
||||||
|
<n-input
|
||||||
|
v-model:value="formData.title"
|
||||||
|
placeholder="请输入文档标题"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<!-- 文本类型 -->
|
||||||
|
<template v-if="formData.type === 'text'">
|
||||||
|
<n-form-item label="文档内容" path="content">
|
||||||
|
<n-input
|
||||||
|
v-model:value="formData.content"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="请输入文档内容"
|
||||||
|
:autosize="{ minRows: 10, maxRows: 20 }"
|
||||||
|
show-count
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 文件类型 -->
|
||||||
|
<template v-if="formData.type === 'file'">
|
||||||
|
<n-form-item label="文件上传" path="filePath">
|
||||||
|
<n-upload
|
||||||
|
ref="uploadRef"
|
||||||
|
:action="uploadAction"
|
||||||
|
:headers="uploadHeaders"
|
||||||
|
:data="uploadData"
|
||||||
|
:file-list="fileList"
|
||||||
|
:max="1"
|
||||||
|
@before-upload="handleBeforeUpload"
|
||||||
|
@finish="handleUploadFinish"
|
||||||
|
@error="handleUploadError"
|
||||||
|
@remove="handleRemoveFile"
|
||||||
|
>
|
||||||
|
<n-upload-dragger>
|
||||||
|
<div style="margin-bottom: 12px">
|
||||||
|
<n-icon size="48" :depth="3">
|
||||||
|
<UploadOutlined />
|
||||||
|
</n-icon>
|
||||||
|
</div>
|
||||||
|
<n-text style="font-size: 16px">
|
||||||
|
点击或者拖动文件到该区域来上传
|
||||||
|
</n-text>
|
||||||
|
<n-p depth="3" style="margin: 8px 0 0 0">
|
||||||
|
支持 PDF、Word、Excel、PowerPoint、TXT 等格式
|
||||||
|
</n-p>
|
||||||
|
</n-upload-dragger>
|
||||||
|
</n-upload>
|
||||||
|
</n-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- URL类型 -->
|
||||||
|
<template v-if="formData.type === 'url'">
|
||||||
|
<n-form-item label="网页链接" path="content">
|
||||||
|
<n-input
|
||||||
|
v-model:value="formData.content"
|
||||||
|
placeholder="请输入网页链接地址"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</template>
|
||||||
|
</n-form>
|
||||||
|
|
||||||
|
<template #action>
|
||||||
|
<n-space>
|
||||||
|
<n-button @click="handleCancel">取消</n-button>
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
:loading="submitLoading"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
|
{{ isEdit ? '更新' : '创建' }}
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, computed, watch } from 'vue'
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
import { UploadOutlined } from '@vicons/antd'
|
||||||
|
import type { FormInst, FormRules, UploadFileInfo } from 'naive-ui'
|
||||||
|
import { saveKnowledgeDoc } from '../api/knowledge'
|
||||||
|
import type { KnowledgeDoc, KnowledgeDocFormData } from '../types/knowledge'
|
||||||
|
import { getHeaders } from '../utils/file'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
show: boolean
|
||||||
|
knowledgeId: string
|
||||||
|
docData?: Partial<KnowledgeDoc>
|
||||||
|
isEdit?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:show', value: boolean): void
|
||||||
|
(e: 'success'): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
show: false,
|
||||||
|
isEdit: false,
|
||||||
|
docData: () => ({})
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
|
// 组件引用
|
||||||
|
const formRef = ref<FormInst>()
|
||||||
|
const uploadRef = ref()
|
||||||
|
|
||||||
|
// 消息
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const formData = reactive<KnowledgeDocFormData>({
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
type: 'text',
|
||||||
|
filePath: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const submitLoading = ref(false)
|
||||||
|
const fileList = ref<UploadFileInfo[]>([])
|
||||||
|
|
||||||
|
// 上传配置
|
||||||
|
const uploadAction = '/api/sys/common/upload'
|
||||||
|
const uploadHeaders = computed(() => getHeaders())
|
||||||
|
const uploadData = { biz: 'knowledge_doc' }
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const showModal = computed({
|
||||||
|
get: () => props.show,
|
||||||
|
set: (value) => emit('update:show', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const modalTitle = computed(() => {
|
||||||
|
const typeMap = {
|
||||||
|
text: '文本文档',
|
||||||
|
file: '文件文档',
|
||||||
|
url: '网页文档'
|
||||||
|
}
|
||||||
|
const typeText = typeMap[formData.type] || '文档'
|
||||||
|
return props.isEdit ? `编辑${typeText}` : `创建${typeText}`
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const formRules: FormRules = {
|
||||||
|
title: [
|
||||||
|
{ required: true, message: '请输入文档标题', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: () => {
|
||||||
|
if (formData.type === 'text') return '请输入文档内容'
|
||||||
|
if (formData.type === 'url') return '请输入网页链接'
|
||||||
|
return '请输入内容'
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
filePath: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请上传文件',
|
||||||
|
trigger: 'change',
|
||||||
|
validator: () => {
|
||||||
|
if (formData.type === 'file') {
|
||||||
|
return !!formData.filePath || fileList.value.length > 0
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听弹窗显示状态
|
||||||
|
watch(() => props.show, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
initForm()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 初始化表单
|
||||||
|
const initForm = () => {
|
||||||
|
if (props.isEdit && props.docData) {
|
||||||
|
Object.assign(formData, {
|
||||||
|
title: props.docData.title || '',
|
||||||
|
content: props.docData.content || '',
|
||||||
|
type: props.docData.type || 'text',
|
||||||
|
filePath: props.docData.filePath || ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 如果是文件类型且有文件路径,初始化文件列表
|
||||||
|
if (formData.type === 'file' && formData.filePath) {
|
||||||
|
fileList.value = [{
|
||||||
|
id: '1',
|
||||||
|
name: formData.filePath.split('/').pop() || 'file',
|
||||||
|
status: 'finished',
|
||||||
|
url: formData.filePath
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 重置表单
|
||||||
|
Object.assign(formData, {
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
type: props.docData?.type || 'text',
|
||||||
|
filePath: ''
|
||||||
|
})
|
||||||
|
fileList.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件上传前处理
|
||||||
|
const handleBeforeUpload = (data: { file: File; fileList: UploadFileInfo[] }) => {
|
||||||
|
const { file } = data
|
||||||
|
|
||||||
|
// 检查文件大小(限制为50MB)
|
||||||
|
if (file.size && file.size > 50 * 1024 * 1024) {
|
||||||
|
message.error('文件大小不能超过50MB')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件类型
|
||||||
|
const allowedTypes = [
|
||||||
|
'application/pdf',
|
||||||
|
'application/msword',
|
||||||
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
'application/vnd.ms-excel',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
'application/vnd.ms-powerpoint',
|
||||||
|
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||||
|
'text/plain'
|
||||||
|
]
|
||||||
|
|
||||||
|
if (!allowedTypes.includes(file.type)) {
|
||||||
|
message.error('不支持的文件类型')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件上传完成
|
||||||
|
const handleUploadFinish = ({ event }: { file: UploadFileInfo; event?: ProgressEvent }) => {
|
||||||
|
try {
|
||||||
|
const response = JSON.parse((event?.target as XMLHttpRequest)?.response || '{}')
|
||||||
|
if (response.success) {
|
||||||
|
formData.filePath = response.result.url
|
||||||
|
message.success('文件上传成功')
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '文件上传失败')
|
||||||
|
fileList.value = []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('文件上传失败')
|
||||||
|
fileList.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件上传错误
|
||||||
|
const handleUploadError = () => {
|
||||||
|
message.error('文件上传失败')
|
||||||
|
fileList.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除文件
|
||||||
|
const handleRemoveFile = () => {
|
||||||
|
formData.filePath = ''
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
await formRef.value?.validate()
|
||||||
|
submitLoading.value = true
|
||||||
|
|
||||||
|
const submitData: any = {
|
||||||
|
...formData,
|
||||||
|
knowledgeId: props.knowledgeId
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.isEdit && props.docData?.id) {
|
||||||
|
submitData.id = props.docData.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是文件类型,处理metadata
|
||||||
|
if (formData.type === 'file' && formData.filePath) {
|
||||||
|
submitData.metadata = JSON.stringify({ filePath: formData.filePath })
|
||||||
|
delete submitData.filePath
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveKnowledgeDoc(submitData)
|
||||||
|
message.success(props.isEdit ? '更新成功' : '创建成功')
|
||||||
|
|
||||||
|
showModal.value = false
|
||||||
|
emit('success')
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
message.error(error.message)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
submitLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消
|
||||||
|
const handleCancel = () => {
|
||||||
|
showModal.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.n-form {
|
||||||
|
.n-form-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-upload-dragger {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,222 @@
|
|||||||
|
<template>
|
||||||
|
<n-modal
|
||||||
|
v-model:show="showModal"
|
||||||
|
preset="card"
|
||||||
|
title="段落详情"
|
||||||
|
style="width: 800px; max-height: 80vh"
|
||||||
|
:mask-closable="true"
|
||||||
|
:closable="true"
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
|
<div class="text-desc-container">
|
||||||
|
<!-- 头部信息 -->
|
||||||
|
<div class="header">
|
||||||
|
<n-tag type="info" size="medium">
|
||||||
|
{{ textData.source }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag v-if="textData.score" type="success" size="medium">
|
||||||
|
相似度: {{ textData.score.toFixed(4) }}
|
||||||
|
</n-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
<div class="content">
|
||||||
|
<n-scrollbar style="max-height: 500px">
|
||||||
|
<div class="markdown-content" v-html="renderedContent"></div>
|
||||||
|
</n-scrollbar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作区域 -->
|
||||||
|
<div class="actions">
|
||||||
|
<n-space>
|
||||||
|
<n-button @click="handleCopy">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon><CopyOutlined /></n-icon>
|
||||||
|
</template>
|
||||||
|
复制内容
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="handleClose">关闭</n-button>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
import { CopyOutlined } from '@vicons/antd'
|
||||||
|
import { copyToClipboard } from '../utils/clipboard'
|
||||||
|
import { replaceImageWith, replaceDomainUrl } from '../utils/common'
|
||||||
|
import { marked } from 'marked'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
show: boolean
|
||||||
|
textData?: {
|
||||||
|
content: string
|
||||||
|
source?: string
|
||||||
|
score?: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:show', value: boolean): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
show: false,
|
||||||
|
textData: () => ({
|
||||||
|
content: '',
|
||||||
|
source: '',
|
||||||
|
score: 0
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
|
// 消息
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const showModal = computed({
|
||||||
|
get: () => props.show,
|
||||||
|
set: (value) => emit('update:show', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const renderedContent = computed(() => {
|
||||||
|
if (!props.textData?.content) return ''
|
||||||
|
|
||||||
|
let content = props.textData.content
|
||||||
|
|
||||||
|
// 替换图片宽度
|
||||||
|
content = replaceImageWith(content)
|
||||||
|
|
||||||
|
// 替换域名URL
|
||||||
|
content = replaceDomainUrl(content)
|
||||||
|
|
||||||
|
// 如果内容包含Markdown语法,进行渲染
|
||||||
|
if (content.includes('**') || content.includes('##') || content.includes('[') || content.includes('```')) {
|
||||||
|
try {
|
||||||
|
return marked(content)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Markdown渲染失败,使用原始内容:', error)
|
||||||
|
return content.replace(/\n/g, '<br>')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通文本,转换换行符
|
||||||
|
return content.replace(/\n/g, '<br>')
|
||||||
|
})
|
||||||
|
|
||||||
|
// 复制内容
|
||||||
|
const handleCopy = async () => {
|
||||||
|
try {
|
||||||
|
const success = await copyToClipboard(props.textData?.content || '')
|
||||||
|
if (success) {
|
||||||
|
message.success('复制成功')
|
||||||
|
} else {
|
||||||
|
message.error('复制失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('复制失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
const handleClose = () => {
|
||||||
|
showModal.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.text-desc-container {
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.markdown-content {
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
:deep(h1), :deep(h2), :deep(h3), :deep(h4), :deep(h5), :deep(h6) {
|
||||||
|
margin: 16px 0 8px 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(p) {
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(ul), :deep(ol) {
|
||||||
|
margin: 8px 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(blockquote) {
|
||||||
|
margin: 16px 0;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-left: 4px solid #dee2e6;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(code) {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(pre) {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
code {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(img) {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(table) {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 16px 0;
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
padding: 8px 12px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
88
src/views/teacher/ai-knowledge-naive-ui/example/App.vue
Normal file
88
src/views/teacher/ai-knowledge-naive-ui/example/App.vue
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<template>
|
||||||
|
<n-config-provider :theme="theme">
|
||||||
|
<n-global-style />
|
||||||
|
<n-message-provider>
|
||||||
|
<n-dialog-provider>
|
||||||
|
<div class="app-container">
|
||||||
|
<!-- 头部导航 -->
|
||||||
|
<n-layout-header bordered style="height: 64px; padding: 0 24px">
|
||||||
|
<div class="header-content">
|
||||||
|
<div class="logo">
|
||||||
|
<n-icon size="32" color="#1890ff">
|
||||||
|
<DatabaseOutlined />
|
||||||
|
</n-icon>
|
||||||
|
<span class="title">AI知识库管理系统</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-actions">
|
||||||
|
<n-button
|
||||||
|
quaternary
|
||||||
|
circle
|
||||||
|
@click="toggleTheme"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<BulbOutlined v-if="isDark" />
|
||||||
|
<BulbFilled v-else />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-layout-header>
|
||||||
|
|
||||||
|
<!-- 主要内容区域 -->
|
||||||
|
<n-layout-content style="height: calc(100vh - 64px)">
|
||||||
|
<AiKnowledgeBaseList />
|
||||||
|
</n-layout-content>
|
||||||
|
</div>
|
||||||
|
</n-dialog-provider>
|
||||||
|
</n-message-provider>
|
||||||
|
</n-config-provider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { darkTheme } from 'naive-ui'
|
||||||
|
import { DatabaseOutlined, BulbOutlined, BulbFilled } from '@vicons/antd'
|
||||||
|
import AiKnowledgeBaseList from '../AiKnowledgeBaseList.vue'
|
||||||
|
|
||||||
|
// 主题切换
|
||||||
|
const isDark = ref(false)
|
||||||
|
const theme = computed(() => isDark.value ? darkTheme : null)
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
isDark.value = !isDark.value
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.app-container {
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
23
src/views/teacher/ai-knowledge-naive-ui/index.ts
Normal file
23
src/views/teacher/ai-knowledge-naive-ui/index.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* AI知识库管理系统 - Naive UI版本
|
||||||
|
* 导出所有组件和工具函数
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 主要组件
|
||||||
|
export { default as AiKnowledgeBaseList } from './AiKnowledgeBaseList.vue'
|
||||||
|
export { default as KnowledgeBaseModal } from './components/KnowledgeBaseModal.vue'
|
||||||
|
export { default as KnowledgeDocListModal } from './components/KnowledgeDocListModal.vue'
|
||||||
|
export { default as KnowledgeDocTextModal } from './components/KnowledgeDocTextModal.vue'
|
||||||
|
export { default as TextDescModal } from './components/TextDescModal.vue'
|
||||||
|
|
||||||
|
// API接口
|
||||||
|
export * from './api/knowledge'
|
||||||
|
|
||||||
|
// 类型定义
|
||||||
|
export * from './types/knowledge'
|
||||||
|
|
||||||
|
// 工具函数
|
||||||
|
export * from './utils/http'
|
||||||
|
export * from './utils/clipboard'
|
||||||
|
export * from './utils/file'
|
||||||
|
export * from './utils/common'
|
58
src/views/teacher/ai-knowledge-naive-ui/package.json
Normal file
58
src/views/teacher/ai-knowledge-naive-ui/package.json
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"name": "ai-knowledge-naive-ui",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "AI知识库管理系统 - Naive UI版本",
|
||||||
|
"main": "index.ts",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"type-check": "vue-tsc --noEmit"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"vue3",
|
||||||
|
"typescript",
|
||||||
|
"naive-ui",
|
||||||
|
"ai",
|
||||||
|
"knowledge-base",
|
||||||
|
"vector-database"
|
||||||
|
],
|
||||||
|
"author": "Your Name",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "^3.3.0",
|
||||||
|
"naive-ui": "^2.35.0",
|
||||||
|
"@vicons/antd": "^0.12.0",
|
||||||
|
"axios": "^1.5.0",
|
||||||
|
"marked": "^9.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.0.0",
|
||||||
|
"@vitejs/plugin-vue": "^4.4.0",
|
||||||
|
"typescript": "^5.2.0",
|
||||||
|
"vite": "^4.4.0",
|
||||||
|
"vue-tsc": "^1.8.0",
|
||||||
|
"less": "^4.2.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": ">=3.3.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"*.vue",
|
||||||
|
"api/",
|
||||||
|
"components/",
|
||||||
|
"types/",
|
||||||
|
"utils/",
|
||||||
|
"index.ts",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/your-username/ai-knowledge-naive-ui.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/your-username/ai-knowledge-naive-ui/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/your-username/ai-knowledge-naive-ui#readme"
|
||||||
|
}
|
46
src/views/teacher/ai-knowledge-naive-ui/tsconfig.json
Normal file
46
src/views/teacher/ai-knowledge-naive-ui/tsconfig.json
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
/* Path mapping */
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Vue specific */
|
||||||
|
"types": ["vite/client", "node"]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
"**/*.vue"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
11
src/views/teacher/ai-knowledge-naive-ui/tsconfig.node.json
Normal file
11
src/views/teacher/ai-knowledge-naive-ui/tsconfig.node.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
174
src/views/teacher/ai-knowledge-naive-ui/types/knowledge.ts
Normal file
174
src/views/teacher/ai-knowledge-naive-ui/types/knowledge.ts
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
/**
|
||||||
|
* AI知识库相关类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 知识库基础信息
|
||||||
|
export interface KnowledgeBase {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
descr?: string
|
||||||
|
embedId: string
|
||||||
|
embedName?: string
|
||||||
|
status?: 'active' | 'inactive'
|
||||||
|
createBy?: string
|
||||||
|
createBy_dictText?: string
|
||||||
|
createTime?: string
|
||||||
|
updateBy?: string
|
||||||
|
updateTime?: string
|
||||||
|
docCount?: number
|
||||||
|
vectorCount?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 知识库搜索表单
|
||||||
|
export interface KnowledgeSearchForm {
|
||||||
|
name?: string
|
||||||
|
status?: string
|
||||||
|
createBy?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 知识库文档
|
||||||
|
export interface KnowledgeDoc {
|
||||||
|
id: string
|
||||||
|
knowledgeId: string
|
||||||
|
title: string
|
||||||
|
content?: string
|
||||||
|
type: 'text' | 'file' | 'url'
|
||||||
|
source?: string
|
||||||
|
metadata?: string
|
||||||
|
filePath?: string
|
||||||
|
status?: 'processing' | 'completed' | 'failed'
|
||||||
|
createBy?: string
|
||||||
|
createBy_dictText?: string
|
||||||
|
createTime?: string
|
||||||
|
updateBy?: string
|
||||||
|
updateTime?: string
|
||||||
|
vectorStatus?: 'pending' | 'processing' | 'completed' | 'failed'
|
||||||
|
__checked?: boolean // 用于表格选择
|
||||||
|
}
|
||||||
|
|
||||||
|
// 知识库文档搜索表单
|
||||||
|
export interface KnowledgeDocSearchForm {
|
||||||
|
title?: string
|
||||||
|
type?: string
|
||||||
|
status?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 向量化命中测试结果
|
||||||
|
export interface EmbeddingHitResult {
|
||||||
|
id: string
|
||||||
|
content: string
|
||||||
|
source: string
|
||||||
|
score: number
|
||||||
|
metadata?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页参数
|
||||||
|
export interface Pagination {
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表单规则
|
||||||
|
export interface FormRules {
|
||||||
|
[key: string]: Array<{
|
||||||
|
required?: boolean
|
||||||
|
message: string
|
||||||
|
trigger?: string | string[]
|
||||||
|
min?: number
|
||||||
|
max?: number
|
||||||
|
pattern?: RegExp
|
||||||
|
validator?: (rule: any, value: any) => boolean | Promise<boolean>
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
// 知识库表单数据
|
||||||
|
export interface KnowledgeFormData {
|
||||||
|
id?: string
|
||||||
|
name: string
|
||||||
|
descr?: string
|
||||||
|
embedId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 知识库文档表单数据
|
||||||
|
export interface KnowledgeDocFormData {
|
||||||
|
id?: string
|
||||||
|
knowledgeId?: string
|
||||||
|
title: string
|
||||||
|
content?: string
|
||||||
|
type: 'text' | 'file' | 'url'
|
||||||
|
filePath?: string
|
||||||
|
metadata?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件上传响应
|
||||||
|
export interface UploadResponse {
|
||||||
|
success: boolean
|
||||||
|
message: string
|
||||||
|
result: {
|
||||||
|
filename: string
|
||||||
|
url: string
|
||||||
|
size: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 向量模型选项
|
||||||
|
export interface EmbedModelOption {
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// 菜单项
|
||||||
|
export interface MenuItem {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
icon?: string
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表格列配置
|
||||||
|
export interface TableColumn {
|
||||||
|
key: string
|
||||||
|
title: string
|
||||||
|
width?: number
|
||||||
|
align?: 'left' | 'center' | 'right'
|
||||||
|
ellipsis?: boolean
|
||||||
|
render?: (row: any) => any
|
||||||
|
}
|
||||||
|
|
||||||
|
// 操作按钮配置
|
||||||
|
export interface ActionButton {
|
||||||
|
label: string
|
||||||
|
type?: 'primary' | 'info' | 'success' | 'warning' | 'error'
|
||||||
|
icon?: string
|
||||||
|
disabled?: boolean
|
||||||
|
loading?: boolean
|
||||||
|
onClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
// 知识库统计信息
|
||||||
|
export interface KnowledgeStats {
|
||||||
|
totalKnowledge: number
|
||||||
|
totalDocs: number
|
||||||
|
totalVectors: number
|
||||||
|
processingDocs: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量操作参数
|
||||||
|
export interface BatchOperationParams {
|
||||||
|
ids: string[]
|
||||||
|
operation: 'delete' | 'rebuild' | 'enable' | 'disable'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重建向量参数
|
||||||
|
export interface RebuildVectorParams {
|
||||||
|
knowIds?: string
|
||||||
|
docIds?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空文档参数
|
||||||
|
export interface DeleteAllDocsParams {
|
||||||
|
knowledgeId: string
|
||||||
|
confirm: boolean
|
||||||
|
}
|
48
src/views/teacher/ai-knowledge-naive-ui/utils/clipboard.ts
Normal file
48
src/views/teacher/ai-knowledge-naive-ui/utils/clipboard.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* 复制文本到剪贴板
|
||||||
|
* @param text 要复制的文本
|
||||||
|
* @returns Promise<boolean> 是否复制成功
|
||||||
|
*/
|
||||||
|
export async function copyToClipboard(text: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// 优先使用现代的 Clipboard API
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 降级方案:使用 document.execCommand
|
||||||
|
const textArea = document.createElement('textarea')
|
||||||
|
textArea.value = text
|
||||||
|
textArea.style.position = 'fixed'
|
||||||
|
textArea.style.left = '-999999px'
|
||||||
|
textArea.style.top = '-999999px'
|
||||||
|
document.body.appendChild(textArea)
|
||||||
|
textArea.focus()
|
||||||
|
textArea.select()
|
||||||
|
|
||||||
|
const success = document.execCommand('copy')
|
||||||
|
document.body.removeChild(textArea)
|
||||||
|
|
||||||
|
return success
|
||||||
|
} catch (error) {
|
||||||
|
console.error('复制到剪贴板失败:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从剪贴板读取文本
|
||||||
|
* @returns Promise<string> 剪贴板中的文本
|
||||||
|
*/
|
||||||
|
export async function readFromClipboard(): Promise<string> {
|
||||||
|
try {
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
return await navigator.clipboard.readText()
|
||||||
|
}
|
||||||
|
throw new Error('Clipboard API not supported')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('从剪贴板读取失败:', error)
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
173
src/views/teacher/ai-knowledge-naive-ui/utils/common.ts
Normal file
173
src/views/teacher/ai-knowledge-naive-ui/utils/common.ts
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
/**
|
||||||
|
* 通用工具函数
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 防抖函数
|
||||||
|
* @param func 要防抖的函数
|
||||||
|
* @param wait 等待时间(毫秒)
|
||||||
|
* @returns 防抖后的函数
|
||||||
|
*/
|
||||||
|
export function debounce<T extends (...args: any[]) => any>(
|
||||||
|
func: T,
|
||||||
|
wait: number
|
||||||
|
): (...args: Parameters<T>) => void {
|
||||||
|
let timeout: NodeJS.Timeout | null = null
|
||||||
|
|
||||||
|
return function (this: any, ...args: Parameters<T>) {
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
timeout = setTimeout(() => func.apply(this, args), wait)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节流函数
|
||||||
|
* @param func 要节流的函数
|
||||||
|
* @param wait 等待时间(毫秒)
|
||||||
|
* @returns 节流后的函数
|
||||||
|
*/
|
||||||
|
export function throttle<T extends (...args: any[]) => any>(
|
||||||
|
func: T,
|
||||||
|
wait: number
|
||||||
|
): (...args: Parameters<T>) => void {
|
||||||
|
let timeout: NodeJS.Timeout | null = null
|
||||||
|
let previous = 0
|
||||||
|
|
||||||
|
return function (this: any, ...args: Parameters<T>) {
|
||||||
|
const now = Date.now()
|
||||||
|
const remaining = wait - (now - previous)
|
||||||
|
const context = this
|
||||||
|
|
||||||
|
if (remaining <= 0 || remaining > wait) {
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
timeout = null
|
||||||
|
}
|
||||||
|
previous = now
|
||||||
|
func.apply(context, args)
|
||||||
|
} else if (!timeout) {
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
previous = Date.now()
|
||||||
|
timeout = null
|
||||||
|
func.apply(context, args)
|
||||||
|
}, remaining)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 深拷贝对象
|
||||||
|
* @param obj 要拷贝的对象
|
||||||
|
* @returns 拷贝后的对象
|
||||||
|
*/
|
||||||
|
export function deepClone<T>(obj: T): T {
|
||||||
|
if (obj === null || typeof obj !== 'object') {
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj instanceof Date) {
|
||||||
|
return new Date(obj.getTime()) as T
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj instanceof Array) {
|
||||||
|
return obj.map(item => deepClone(item)) as T
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === 'object') {
|
||||||
|
const clonedObj = {} as T
|
||||||
|
for (const key in obj) {
|
||||||
|
if (obj.hasOwnProperty(key)) {
|
||||||
|
clonedObj[key] = deepClone(obj[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clonedObj
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成唯一ID
|
||||||
|
* @returns 唯一ID字符串
|
||||||
|
*/
|
||||||
|
export function generateId(): string {
|
||||||
|
return Math.random().toString(36).substr(2, 9) + Date.now().toString(36)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期
|
||||||
|
* @param date 日期对象或时间戳
|
||||||
|
* @param format 格式字符串,默认为 'YYYY-MM-DD HH:mm:ss'
|
||||||
|
* @returns 格式化后的日期字符串
|
||||||
|
*/
|
||||||
|
export function formatDate(date: Date | number | string, format = 'YYYY-MM-DD HH:mm:ss'): string {
|
||||||
|
const d = new Date(date)
|
||||||
|
|
||||||
|
if (isNaN(d.getTime())) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const year = d.getFullYear()
|
||||||
|
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(d.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(d.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(d.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(d.getSeconds()).padStart(2, '0')
|
||||||
|
|
||||||
|
return format
|
||||||
|
.replace('YYYY', year.toString())
|
||||||
|
.replace('MM', month)
|
||||||
|
.replace('DD', day)
|
||||||
|
.replace('HH', hours)
|
||||||
|
.replace('mm', minutes)
|
||||||
|
.replace('ss', seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否为空值
|
||||||
|
* @param value 要检查的值
|
||||||
|
* @returns 是否为空
|
||||||
|
*/
|
||||||
|
export function isEmpty(value: any): boolean {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value.trim() === ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
return Object.keys(value).length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 替换图片宽度
|
||||||
|
* @param content 内容字符串
|
||||||
|
* @returns 替换后的内容
|
||||||
|
*/
|
||||||
|
export function replaceImageWith(content: string): string {
|
||||||
|
return content.replace(/<img([^>]*?)style="([^"]*?)"([^>]*?)>/g, (_match, before, style, after) => {
|
||||||
|
const newStyle = style.replace(/width:\s*\d+px/g, 'width: 100%')
|
||||||
|
return `<img${before}style="${newStyle}"${after}>`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 替换域名URL
|
||||||
|
* @param content 内容字符串
|
||||||
|
* @returns 替换后的内容
|
||||||
|
*/
|
||||||
|
export function replaceDomainUrl(content: string): string {
|
||||||
|
const baseUrl = import.meta.env.VITE_API_BASE_URL || '/api'
|
||||||
|
return content.replace(/src="\/sys\/common\/static\//g, `src="${baseUrl}/sys/common/static/`)
|
||||||
|
}
|
128
src/views/teacher/ai-knowledge-naive-ui/utils/file.ts
Normal file
128
src/views/teacher/ai-knowledge-naive-ui/utils/file.ts
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
/**
|
||||||
|
* 文件相关工具函数
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件访问URL
|
||||||
|
* @param filePath 文件路径
|
||||||
|
* @returns 完整的文件访问URL
|
||||||
|
*/
|
||||||
|
export function getFileAccessHttpUrl(filePath: string): string {
|
||||||
|
if (!filePath) return ''
|
||||||
|
|
||||||
|
// 如果已经是完整URL,直接返回
|
||||||
|
if (filePath.startsWith('http://') || filePath.startsWith('https://')) {
|
||||||
|
return filePath
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拼接API基础路径
|
||||||
|
const baseUrl = import.meta.env.VITE_API_BASE_URL || '/api'
|
||||||
|
return `${baseUrl}/sys/common/static/${filePath}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取请求头信息
|
||||||
|
* @returns 包含认证信息的请求头
|
||||||
|
*/
|
||||||
|
export function getHeaders(): Record<string, string> {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
return {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token && { Authorization: `Bearer ${token}` })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件大小格式化
|
||||||
|
* @param bytes 字节数
|
||||||
|
* @returns 格式化后的文件大小字符串
|
||||||
|
*/
|
||||||
|
export function formatFileSize(bytes: number): string {
|
||||||
|
if (bytes === 0) return '0 B'
|
||||||
|
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件扩展名
|
||||||
|
* @param filename 文件名
|
||||||
|
* @returns 文件扩展名(不包含点)
|
||||||
|
*/
|
||||||
|
export function getFileExtension(filename: string): string {
|
||||||
|
return filename.slice((filename.lastIndexOf('.') - 1 >>> 0) + 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查文件类型是否为图片
|
||||||
|
* @param filename 文件名或文件类型
|
||||||
|
* @returns 是否为图片类型
|
||||||
|
*/
|
||||||
|
export function isImageFile(filename: string): boolean {
|
||||||
|
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg']
|
||||||
|
const extension = getFileExtension(filename).toLowerCase()
|
||||||
|
return imageExtensions.includes(extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查文件类型是否为文档
|
||||||
|
* @param filename 文件名或文件类型
|
||||||
|
* @returns 是否为文档类型
|
||||||
|
*/
|
||||||
|
export function isDocumentFile(filename: string): boolean {
|
||||||
|
const docExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'md']
|
||||||
|
const extension = getFileExtension(filename).toLowerCase()
|
||||||
|
return docExtensions.includes(extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件下载
|
||||||
|
* @param url 下载链接
|
||||||
|
* @param filename 文件名
|
||||||
|
*/
|
||||||
|
export function downloadFile(url: string, filename?: string): void {
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
if (filename) {
|
||||||
|
link.download = filename
|
||||||
|
}
|
||||||
|
link.target = '_blank'
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将文件转换为Base64
|
||||||
|
* @param file 文件对象
|
||||||
|
* @returns Promise<string> Base64字符串
|
||||||
|
*/
|
||||||
|
export function fileToBase64(file: File): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
reader.onload = () => resolve(reader.result as string)
|
||||||
|
reader.onerror = error => reject(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base64转换为Blob
|
||||||
|
* @param base64 Base64字符串
|
||||||
|
* @param mimeType MIME类型
|
||||||
|
* @returns Blob对象
|
||||||
|
*/
|
||||||
|
export function base64ToBlob(base64: string, mimeType: string): Blob {
|
||||||
|
const byteCharacters = atob(base64.split(',')[1])
|
||||||
|
const byteNumbers = new Array(byteCharacters.length)
|
||||||
|
|
||||||
|
for (let i = 0; i < byteCharacters.length; i++) {
|
||||||
|
byteNumbers[i] = byteCharacters.charCodeAt(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
const byteArray = new Uint8Array(byteNumbers)
|
||||||
|
return new Blob([byteArray], { type: mimeType })
|
||||||
|
}
|
203
src/views/teacher/ai-knowledge-naive-ui/utils/http.ts
Normal file
203
src/views/teacher/ai-knowledge-naive-ui/utils/http.ts
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||||
|
|
||||||
|
export interface HttpResponse<T = any> {
|
||||||
|
success: boolean
|
||||||
|
result: T
|
||||||
|
message: string
|
||||||
|
code: number
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RequestConfig extends AxiosRequestConfig {
|
||||||
|
isTransformResponse?: boolean
|
||||||
|
joinParamsToUrl?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HttpClient {
|
||||||
|
private instance: AxiosInstance
|
||||||
|
|
||||||
|
constructor(config?: AxiosRequestConfig) {
|
||||||
|
this.instance = axios.create({
|
||||||
|
baseURL: '/jeecgboot',
|
||||||
|
timeout: 60000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json;charset=UTF-8'
|
||||||
|
},
|
||||||
|
...config
|
||||||
|
})
|
||||||
|
|
||||||
|
this.setupInterceptors()
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupInterceptors() {
|
||||||
|
// 请求拦截器
|
||||||
|
this.instance.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
// 添加token等认证信息(使用项目标准的 X-Access-Token)
|
||||||
|
const token = localStorage.getItem('X-Access-Token') || sessionStorage.getItem('X-Access-Token')
|
||||||
|
if (token) {
|
||||||
|
config.headers = config.headers || {}
|
||||||
|
config.headers['X-Access-Token'] = token
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加租户ID
|
||||||
|
const tenantId = localStorage.getItem('TENANT_ID') || '0'
|
||||||
|
if (tenantId) {
|
||||||
|
config.headers = config.headers || {}
|
||||||
|
config.headers['X-Tenant-Id'] = tenantId
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 响应拦截器
|
||||||
|
this.instance.interceptors.response.use(
|
||||||
|
(response: AxiosResponse<HttpResponse>) => {
|
||||||
|
const { data } = response
|
||||||
|
|
||||||
|
// 如果是文件下载等特殊情况,直接返回
|
||||||
|
if (response.config.responseType === 'blob') {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查业务状态码
|
||||||
|
if (data.success === false) {
|
||||||
|
throw new Error(data.message || '请求失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
// 处理HTTP错误状态码
|
||||||
|
if (error.response) {
|
||||||
|
const { status, data } = error.response
|
||||||
|
switch (status) {
|
||||||
|
case 401:
|
||||||
|
// 未授权,清除token并跳转到登录页
|
||||||
|
localStorage.removeItem('X-Access-Token')
|
||||||
|
sessionStorage.removeItem('X-Access-Token')
|
||||||
|
if (window.location.pathname !== '/login') {
|
||||||
|
window.location.href = '/login'
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 403:
|
||||||
|
throw new Error('没有权限访问该资源')
|
||||||
|
case 404:
|
||||||
|
throw new Error('请求的资源不存在')
|
||||||
|
case 500:
|
||||||
|
throw new Error('服务器内部错误')
|
||||||
|
default:
|
||||||
|
throw new Error(data?.message || `请求失败 (${status})`)
|
||||||
|
}
|
||||||
|
} else if (error.request) {
|
||||||
|
throw new Error('网络连接失败,请检查网络设置')
|
||||||
|
} else {
|
||||||
|
throw new Error(error.message || '请求配置错误')
|
||||||
|
}
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET请求
|
||||||
|
get<T = any>(config: RequestConfig, options?: { isTransformResponse?: boolean; joinParamsToUrl?: boolean }): Promise<HttpResponse<T>> {
|
||||||
|
const mergedConfig = { ...config, ...options }
|
||||||
|
const { joinParamsToUrl, isTransformResponse = true, ...axiosConfig } = mergedConfig
|
||||||
|
|
||||||
|
if (joinParamsToUrl && mergedConfig.params) {
|
||||||
|
const params = new URLSearchParams(mergedConfig.params).toString()
|
||||||
|
axiosConfig.url = `${axiosConfig.url}?${params}`
|
||||||
|
delete axiosConfig.params
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.instance.get(axiosConfig.url!, axiosConfig).then(response => {
|
||||||
|
return isTransformResponse ? response.data : response
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST请求
|
||||||
|
post<T = any>(config: RequestConfig, options?: { isTransformResponse?: boolean }): Promise<HttpResponse<T>> {
|
||||||
|
const mergedConfig = { ...config, ...options }
|
||||||
|
const { isTransformResponse = true, ...axiosConfig } = mergedConfig
|
||||||
|
return this.instance.post(axiosConfig.url!, axiosConfig.params || axiosConfig.data, axiosConfig).then(response => {
|
||||||
|
return isTransformResponse ? response.data : response
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT请求
|
||||||
|
put<T = any>(config: RequestConfig, options?: { isTransformResponse?: boolean; joinParamsToUrl?: boolean }): Promise<HttpResponse<T>> {
|
||||||
|
const mergedConfig = { ...config, ...options }
|
||||||
|
const { joinParamsToUrl, isTransformResponse = true, ...axiosConfig } = mergedConfig
|
||||||
|
|
||||||
|
if (joinParamsToUrl && mergedConfig.params) {
|
||||||
|
const params = new URLSearchParams(mergedConfig.params).toString()
|
||||||
|
axiosConfig.url = `${axiosConfig.url}?${params}`
|
||||||
|
delete axiosConfig.params
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.instance.put(axiosConfig.url!, axiosConfig.params || axiosConfig.data, axiosConfig).then(response => {
|
||||||
|
return isTransformResponse ? response.data : response
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE请求
|
||||||
|
delete<T = any>(config: RequestConfig, options?: { isTransformResponse?: boolean; joinParamsToUrl?: boolean }): Promise<HttpResponse<T>> {
|
||||||
|
const mergedConfig = { ...config, ...options }
|
||||||
|
const { joinParamsToUrl, isTransformResponse = true, ...axiosConfig } = mergedConfig
|
||||||
|
|
||||||
|
if (joinParamsToUrl && mergedConfig.params) {
|
||||||
|
const params = new URLSearchParams(mergedConfig.params).toString()
|
||||||
|
axiosConfig.url = `${axiosConfig.url}?${params}`
|
||||||
|
delete axiosConfig.params
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.instance.delete(axiosConfig.url!, axiosConfig).then(response => {
|
||||||
|
return isTransformResponse ? response.data : response
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件上传
|
||||||
|
upload<T = any>(url: string, formData: FormData, config?: AxiosRequestConfig): Promise<HttpResponse<T>> {
|
||||||
|
return this.instance.post(url, formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
},
|
||||||
|
...config
|
||||||
|
}).then(response => response.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件下载
|
||||||
|
download(url: string, params?: any, filename?: string): Promise<void> {
|
||||||
|
return this.instance.get(url, {
|
||||||
|
params,
|
||||||
|
responseType: 'blob'
|
||||||
|
}).then(response => {
|
||||||
|
const blob = new Blob([response.data])
|
||||||
|
const downloadUrl = window.URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = downloadUrl
|
||||||
|
link.download = filename || 'download'
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
window.URL.revokeObjectURL(downloadUrl)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建默认实例
|
||||||
|
export const http = new HttpClient()
|
||||||
|
|
||||||
|
// 导出默认实例的方法
|
||||||
|
export const defHttp = {
|
||||||
|
get: http.get.bind(http),
|
||||||
|
post: http.post.bind(http),
|
||||||
|
put: http.put.bind(http),
|
||||||
|
delete: http.delete.bind(http),
|
||||||
|
upload: http.upload.bind(http),
|
||||||
|
download: http.download.bind(http)
|
||||||
|
}
|
50
src/views/teacher/ai-knowledge-naive-ui/vite.config.ts
Normal file
50
src/views/teacher/ai-knowledge-naive-ui/vite.config.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import { resolve } from 'path'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': resolve(__dirname, '.'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
preprocessorOptions: {
|
||||||
|
less: {
|
||||||
|
javascriptEnabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
open: true,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8080',
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, '/api'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: resolve(__dirname, 'index.ts'),
|
||||||
|
name: 'AiKnowledgeNaiveUI',
|
||||||
|
fileName: (format) => `ai-knowledge-naive-ui.${format}.js`,
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: ['vue', 'naive-ui', '@vicons/antd', 'axios', 'marked'],
|
||||||
|
output: {
|
||||||
|
globals: {
|
||||||
|
vue: 'Vue',
|
||||||
|
'naive-ui': 'naive',
|
||||||
|
'@vicons/antd': 'ViconsAntd',
|
||||||
|
axios: 'axios',
|
||||||
|
marked: 'marked',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
Loading…
x
Reference in New Issue
Block a user