实现上传功能
This commit is contained in:
parent
1062bcfb70
commit
74df0e539c
172
EDITOR_UPLOAD_FIX.md
Normal file
172
EDITOR_UPLOAD_FIX.md
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
# 编辑器图片上传问题修复
|
||||||
|
|
||||||
|
## 问题描述
|
||||||
|
编辑器里上传的图片无法显示。
|
||||||
|
|
||||||
|
## 根本原因
|
||||||
|
文件保存路径和静态文件URL映射不匹配。
|
||||||
|
|
||||||
|
## 已完成的修改
|
||||||
|
|
||||||
|
### 1. 后端文件保存路径 (`server/controllers/file.go`)
|
||||||
|
|
||||||
|
**修改前:**
|
||||||
|
```go
|
||||||
|
uploadDir := path.Join("uploads", datePath)
|
||||||
|
```
|
||||||
|
文件保存到:`server/uploads/`
|
||||||
|
|
||||||
|
**修改后:**
|
||||||
|
```go
|
||||||
|
uploadDir := path.Join("..", "uploads", datePath)
|
||||||
|
```
|
||||||
|
文件保存到:`项目根目录/uploads/`
|
||||||
|
|
||||||
|
### 2. 静态文件映射 (`server/conf/app.conf`)
|
||||||
|
|
||||||
|
**修改前:**
|
||||||
|
```conf
|
||||||
|
StaticDir = /uploads:uploads
|
||||||
|
```
|
||||||
|
|
||||||
|
**修改后:**
|
||||||
|
```conf
|
||||||
|
StaticDir = /uploads:../uploads
|
||||||
|
```
|
||||||
|
|
||||||
|
现在 `/uploads` URL 正确映射到项目根目录的 `uploads` 文件夹。
|
||||||
|
|
||||||
|
### 3. 前端URL拼接 (`front/src/components/WangEditor.vue`)
|
||||||
|
|
||||||
|
添加了更健壮的URL拼接逻辑:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const fileUrl = response.data.data.file_url; // /uploads/2024/01/15/xxx.jpg
|
||||||
|
const baseUrl = getUploadUrl() ;
|
||||||
|
|
||||||
|
let fullUrl = fileUrl;
|
||||||
|
if (!fileUrl.startsWith('http')) {
|
||||||
|
const base = baseUrl.replace(/\/$/, '');
|
||||||
|
const url = fileUrl.startsWith('/') ? fileUrl : '/' + fileUrl;
|
||||||
|
fullUrl = `${base}${url}`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这样可以确保URL正确拼接为:`http://localhost:8080/uploads/2024/01/15/xxx.jpg`
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
上传后文件将保存在:
|
||||||
|
|
||||||
|
```
|
||||||
|
yunzer_go/
|
||||||
|
├── server/
|
||||||
|
├── front/
|
||||||
|
└── uploads/ ← 文件保存位置
|
||||||
|
├── 2024/
|
||||||
|
│ └── 01/
|
||||||
|
│ └── 15/
|
||||||
|
│ └── 20240115143045_example.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
## 访问URL
|
||||||
|
|
||||||
|
上传后可以通过以下URL访问:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost:8080/uploads/2024/01/15/20240115143045_example.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
## 如何验证
|
||||||
|
|
||||||
|
### 1. 上传一个图片
|
||||||
|
在编辑器中使用图片上传功能上传一张图片。
|
||||||
|
|
||||||
|
### 2. 查看控制台日志
|
||||||
|
打开浏览器开发者工具(F12),查看 Console 标签,应该看到:
|
||||||
|
```
|
||||||
|
图片上传成功,URL: http://localhost:8080/uploads/2024/01/15/xxx.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 检查文件是否保存
|
||||||
|
查看项目根目录的 `uploads` 文件夹:
|
||||||
|
```bash
|
||||||
|
ls uploads/2024/01/15/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 测试URL访问
|
||||||
|
在浏览器中直接访问图片URL,应该能看到图片。
|
||||||
|
|
||||||
|
### 5. 检查数据库
|
||||||
|
查看 `yz_files` 表中的记录:
|
||||||
|
```sql
|
||||||
|
SELECT file_path, file_url FROM yz_files ORDER BY upload_time DESC LIMIT 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
应该看到:
|
||||||
|
- `file_path`: `uploads/2024/01/15/xxx.jpg`
|
||||||
|
- `file_url`: `/uploads/2024/01/15/xxx.jpg`
|
||||||
|
|
||||||
|
## 重启服务器
|
||||||
|
|
||||||
|
修改配置后需要重启服务器才能生效:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd server
|
||||||
|
go run main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
或者如果使用编译后的可执行文件:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./server.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
## 如果还是看不到图片
|
||||||
|
|
||||||
|
### 检查清单
|
||||||
|
|
||||||
|
1. ✅ 服务器是否重启
|
||||||
|
2. ✅ 文件是否保存到 `uploads` 目录
|
||||||
|
3. ✅ 浏览器控制台是否有错误
|
||||||
|
4. ✅ URL是否正确拼接
|
||||||
|
5. ✅ 静态文件映射是否正确
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
**Q: 上传后文件保存在哪里?**
|
||||||
|
A: 项目根目录的 `uploads` 文件夹
|
||||||
|
|
||||||
|
**Q: 图片URL是什么格式?**
|
||||||
|
A: `http://localhost:8080/uploads/2024/01/15/filename.jpg`
|
||||||
|
|
||||||
|
**Q: 为什么看不到图片?**
|
||||||
|
A: 检查:
|
||||||
|
- 文件是否正确保存
|
||||||
|
- URL是否可访问
|
||||||
|
- 浏览器控制台错误信息
|
||||||
|
- 服务器是否重启
|
||||||
|
|
||||||
|
**Q: 如何自定义上传目录?**
|
||||||
|
A: 修改 `server/controllers/file.go` 和 `server/conf/app.conf` 中的路径配置
|
||||||
|
|
||||||
|
## 调试建议
|
||||||
|
|
||||||
|
如果图片仍不显示,检查以下内容:
|
||||||
|
|
||||||
|
1. **查看浏览器网络请求**
|
||||||
|
- F12 → Network 标签
|
||||||
|
- 查看图片请求是否返回 200
|
||||||
|
- 如果返回 404,说明路径不对
|
||||||
|
|
||||||
|
2. **查看浏览器控制台**
|
||||||
|
- 查看是否有 CORS 错误
|
||||||
|
- 查看上传成功后的 URL 是什么
|
||||||
|
|
||||||
|
3. **查看服务器日志**
|
||||||
|
- 确认文件是否正确上传
|
||||||
|
- 确认路径是否正确
|
||||||
|
|
||||||
|
4. **检查文件权限**
|
||||||
|
- 确保应用有创建目录和写入文件的权限
|
||||||
|
|
||||||
@ -18,7 +18,14 @@ export const fileAPI = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 上传文件
|
// 上传文件
|
||||||
uploadFile: (formData: FormData) => {
|
uploadFile: (formData: FormData, options?: { category?: string; tenantId?: string }) => {
|
||||||
|
// 如果提供了额外参数,添加到 formData 中
|
||||||
|
if (options?.category) {
|
||||||
|
formData.append('category', options.category)
|
||||||
|
}
|
||||||
|
if (options?.tenantId) {
|
||||||
|
formData.append('tenant_id', options.tenantId)
|
||||||
|
}
|
||||||
return api.post('/api/files', formData, {
|
return api.post('/api/files', formData, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data'
|
'Content-Type': 'multipart/form-data'
|
||||||
|
|||||||
204
front/src/components/WANGEDITOR_UPLOAD.md
Normal file
204
front/src/components/WANGEDITOR_UPLOAD.md
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
# WangEditor 文件上传功能说明
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
|
||||||
|
WangEditor 组件现已集成文件上传功能,支持:
|
||||||
|
- **图片上传**:5MB 以内
|
||||||
|
- **视频上传**:100MB 以内
|
||||||
|
- **附件上传**:50MB 以内
|
||||||
|
|
||||||
|
所有文件都会上传到服务器并保存在 `front/uploads` 目录下,按照日期自动分类。
|
||||||
|
|
||||||
|
## 已实现的功能
|
||||||
|
|
||||||
|
### 1. 图片上传
|
||||||
|
- 通过工具栏的图片按钮上传
|
||||||
|
- 自动插入到编辑器中
|
||||||
|
- 文件保存在服务器并生成可访问的 URL
|
||||||
|
|
||||||
|
### 2. 视频上传
|
||||||
|
- 通过工具栏的视频按钮上传
|
||||||
|
- 自动插入视频播放器
|
||||||
|
- 支持常见的视频格式
|
||||||
|
|
||||||
|
### 3. 附件上传
|
||||||
|
- 支持所有文件类型
|
||||||
|
- 自动生成下载链接
|
||||||
|
- 文件名自动保留
|
||||||
|
|
||||||
|
## 技术实现
|
||||||
|
|
||||||
|
### 上传配置
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
MENU_CONF: {
|
||||||
|
// 图片上传
|
||||||
|
uploadImage: {
|
||||||
|
server: '/api/files',
|
||||||
|
fieldName: 'file',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
customUpload: async (file, insertFn) => {
|
||||||
|
await handleUploadImage(file, insertFn);
|
||||||
|
},
|
||||||
|
allowedFileTypes: ['image/*'],
|
||||||
|
maxFileSize: 5 * 1024 * 1024,
|
||||||
|
},
|
||||||
|
// 视频上传
|
||||||
|
uploadVideo: {
|
||||||
|
server: '/api/files',
|
||||||
|
fieldName: 'file',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
customUpload: async (file, insertFn) => {
|
||||||
|
await handleUploadVideo(file, insertFn);
|
||||||
|
},
|
||||||
|
allowedFileTypes: ['video/*'],
|
||||||
|
maxFileSize: 100 * 1024 * 1024,
|
||||||
|
},
|
||||||
|
// 附件上传
|
||||||
|
uploadAttachment: {
|
||||||
|
server: '/api/files',
|
||||||
|
fieldName: 'file',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
customUpload: async (file, insertFn) => {
|
||||||
|
await handleUploadAttachment(file, insertFn);
|
||||||
|
},
|
||||||
|
allowedFileTypes: ['*'],
|
||||||
|
maxFileSize: 50 * 1024 * 1024,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 上传流程
|
||||||
|
|
||||||
|
1. **用户选择文件** → wangEditor 触发上传
|
||||||
|
2. **创建 FormData** → 包装文件数据
|
||||||
|
3. **调用 fileAPI** → 发送到后端 `/api/files`
|
||||||
|
4. **后端保存** → 文件保存到 `front/uploads/年/月/日/`
|
||||||
|
5. **返回 URL** → 后端返回文件访问 URL
|
||||||
|
6. **插入编辑器** → 将 URL 插入到编辑器内容中
|
||||||
|
|
||||||
|
### 后端响应格式
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "文件上传成功",
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"file_url": "/uploads/2024/01/15/20240115143045_example.jpg",
|
||||||
|
"file_path": "uploads/2024/01/15/20240115143045_example.jpg",
|
||||||
|
"file_name": "example",
|
||||||
|
"original_name": "example.jpg",
|
||||||
|
"file_size": 102400,
|
||||||
|
"file_type": "image",
|
||||||
|
"file_ext": ".jpg",
|
||||||
|
"category": "编辑器"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
### 基础使用
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<WangEditor v-model="content" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import WangEditor from '@/components/WangEditor.vue'
|
||||||
|
|
||||||
|
const content = ref('')
|
||||||
|
|
||||||
|
// 编辑器会自动支持上传功能
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自定义配置
|
||||||
|
|
||||||
|
如果需要修改上传限制,可以编辑 `WangEditor.vue` 中的配置:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 修改文件大小限制
|
||||||
|
maxFileSize: 10 * 1024 * 1024, // 改为 10MB
|
||||||
|
|
||||||
|
// 修改允许的文件类型
|
||||||
|
allowedFileTypes: ['image/jpeg', 'image/png'],
|
||||||
|
|
||||||
|
// 修改分类
|
||||||
|
category: '自定义分类',
|
||||||
|
```
|
||||||
|
|
||||||
|
## 文件存储位置
|
||||||
|
|
||||||
|
所有上传的文件按日期分类存储在:
|
||||||
|
|
||||||
|
```
|
||||||
|
front/uploads/
|
||||||
|
├── 2024/
|
||||||
|
│ ├── 01/
|
||||||
|
│ │ ├── 15/
|
||||||
|
│ │ │ ├── 20240115143045_image.jpg
|
||||||
|
│ │ │ └── 20240115143046_video.mp4
|
||||||
|
```
|
||||||
|
|
||||||
|
## 访问已上传的文件
|
||||||
|
|
||||||
|
上传后的文件可以通过以下 URL 访问:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost:8080/uploads/2024/01/15/20240115143045_image.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **文件大小限制**
|
||||||
|
- 图片:5MB
|
||||||
|
- 视频:100MB
|
||||||
|
- 附件:50MB
|
||||||
|
|
||||||
|
2. **文件类型验证**
|
||||||
|
- 图片:所有图片格式
|
||||||
|
- 视频:所有视频格式
|
||||||
|
- 附件:所有文件格式
|
||||||
|
|
||||||
|
3. **认证要求**
|
||||||
|
- 需要用户登录才能上传
|
||||||
|
- 自动携带 JWT Token
|
||||||
|
|
||||||
|
4. **错误处理**
|
||||||
|
- 上传失败会显示错误消息
|
||||||
|
- 成功后会显示成功提示
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 上传失败
|
||||||
|
|
||||||
|
1. 检查网络连接
|
||||||
|
2. 确认用户已登录
|
||||||
|
3. 检查文件大小是否超限
|
||||||
|
4. 查看浏览器控制台错误信息
|
||||||
|
|
||||||
|
### 图片不显示
|
||||||
|
|
||||||
|
1. 确认文件已成功上传
|
||||||
|
2. 检查文件 URL 是否正确
|
||||||
|
3. 确认服务器已配置静态文件访问
|
||||||
|
4. 检查 CORS 配置
|
||||||
|
|
||||||
|
### 上传按钮不显示
|
||||||
|
|
||||||
|
1. 确认使用了正确的组件
|
||||||
|
2. 检查编辑器初始化是否成功
|
||||||
|
3. 查看浏览器控制台是否有错误
|
||||||
|
|
||||||
|
## 下一步改进建议
|
||||||
|
|
||||||
|
1. 添加上传进度条
|
||||||
|
2. 支持拖拽上传
|
||||||
|
3. 添加图片裁剪功能
|
||||||
|
4. 支持粘贴图片上传
|
||||||
|
5. 添加文件管理器功能
|
||||||
|
|
||||||
@ -7,7 +7,9 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
import { ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
import '@wangeditor/editor/dist/css/style.css';
|
import '@wangeditor/editor/dist/css/style.css';
|
||||||
|
import { fileAPI } from '@/api/file';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: string;
|
modelValue: string;
|
||||||
@ -26,6 +28,119 @@ const editorRef = ref<HTMLDivElement>();
|
|||||||
let editorInstance: any = null;
|
let editorInstance: any = null;
|
||||||
let isDestroyed = false;
|
let isDestroyed = false;
|
||||||
|
|
||||||
|
// 获取上传文件的 URL
|
||||||
|
const getUploadUrl = (): string => {
|
||||||
|
return import.meta.env.VITE_API_BASE_URL;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取 Authorization Header
|
||||||
|
const getAuthHeaders = () => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
return token ? { Authorization: `Bearer ${token}` } : {};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 上传图片处理函数
|
||||||
|
const handleUploadImage = async (file: File, insertFn: (url: string, alt?: string, href?: string) => void) => {
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const response: any = await fileAPI.uploadFile(formData, {
|
||||||
|
category: '编辑器',
|
||||||
|
});
|
||||||
|
|
||||||
|
// axios 拦截器已经返回了 response.data,所以直接使用 response
|
||||||
|
if (response?.success) {
|
||||||
|
const fileUrl = response.data.file_url; // 例如: /uploads/2024/01/15/xxx.jpg
|
||||||
|
const baseUrl = getUploadUrl() || window.location.origin;
|
||||||
|
|
||||||
|
// 处理URL:如果已经是完整URL则直接使用,否则拼接baseUrl
|
||||||
|
let fullUrl = fileUrl;
|
||||||
|
if (!fileUrl.startsWith('http')) {
|
||||||
|
// 确保URL以/开头,baseUrl不以/结尾
|
||||||
|
const base = baseUrl.replace(/\/$/, '');
|
||||||
|
const url = fileUrl.startsWith('/') ? fileUrl : '/' + fileUrl;
|
||||||
|
fullUrl = `${base}${url}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('图片上传成功,URL:', fullUrl);
|
||||||
|
insertFn(fullUrl, file.name, fullUrl);
|
||||||
|
ElMessage.success('图片上传成功');
|
||||||
|
} else {
|
||||||
|
ElMessage.error('上传失败:' + (response?.message || '未知错误'));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Upload error:', error);
|
||||||
|
ElMessage.error('上传失败:' + (error.message || '未知错误'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 上传视频处理函数
|
||||||
|
const handleUploadVideo = async (file: File, insertFn: (url: string, poster?: string) => void) => {
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const response: any = await fileAPI.uploadFile(formData, {
|
||||||
|
category: '编辑器',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response?.success) {
|
||||||
|
const fileUrl = response.data.file_url;
|
||||||
|
const baseUrl = getUploadUrl() || window.location.origin;
|
||||||
|
|
||||||
|
let fullUrl = fileUrl;
|
||||||
|
if (!fileUrl.startsWith('http')) {
|
||||||
|
const base = baseUrl.replace(/\/$/, '');
|
||||||
|
const url = fileUrl.startsWith('/') ? fileUrl : '/' + fileUrl;
|
||||||
|
fullUrl = `${base}${url}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('视频上传成功,URL:', fullUrl);
|
||||||
|
insertFn(fullUrl, '');
|
||||||
|
ElMessage.success('视频上传成功');
|
||||||
|
} else {
|
||||||
|
ElMessage.error('上传失败:' + (response?.message || '未知错误'));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Upload error:', error);
|
||||||
|
ElMessage.error('上传失败:' + (error.message || '未知错误'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 上传附件处理函数
|
||||||
|
const handleUploadAttachment = async (file: File, insertFn: (url: string, text?: string) => void) => {
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const response: any = await fileAPI.uploadFile(formData, {
|
||||||
|
category: '编辑器',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response?.success) {
|
||||||
|
const fileUrl = response.data.file_url;
|
||||||
|
const baseUrl = getUploadUrl() || window.location.origin;
|
||||||
|
|
||||||
|
let fullUrl = fileUrl;
|
||||||
|
if (!fileUrl.startsWith('http')) {
|
||||||
|
const base = baseUrl.replace(/\/$/, '');
|
||||||
|
const url = fileUrl.startsWith('/') ? fileUrl : '/' + fileUrl;
|
||||||
|
fullUrl = `${base}${url}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('附件上传成功,URL:', fullUrl);
|
||||||
|
insertFn(fullUrl, file.name);
|
||||||
|
ElMessage.success('附件上传成功');
|
||||||
|
} else {
|
||||||
|
ElMessage.error('上传失败:' + (response?.message || '未知错误'));
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Upload error:', error);
|
||||||
|
ElMessage.error('上传失败:' + (error.message || '未知错误'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 初始化编辑器
|
// 初始化编辑器
|
||||||
const initEditor = async () => {
|
const initEditor = async () => {
|
||||||
if (!editorRef.value || !toolbarRef.value || isDestroyed) return;
|
if (!editorRef.value || !toolbarRef.value || isDestroyed) return;
|
||||||
@ -42,6 +157,42 @@ const initEditor = async () => {
|
|||||||
emit('update:modelValue', html);
|
emit('update:modelValue', html);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// 自定义上传配置
|
||||||
|
MENU_CONF: {
|
||||||
|
// 图片上传配置
|
||||||
|
uploadImage: {
|
||||||
|
server: '/api/files',
|
||||||
|
fieldName: 'file',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
customUpload: async (file: File, insertFn: (url: string, alt?: string, href?: string) => void) => {
|
||||||
|
await handleUploadImage(file, insertFn);
|
||||||
|
},
|
||||||
|
allowedFileTypes: ['image/*'],
|
||||||
|
maxFileSize: 5 * 1024 * 1024, // 5MB
|
||||||
|
},
|
||||||
|
// 视频上传配置
|
||||||
|
uploadVideo: {
|
||||||
|
server: '/api/files',
|
||||||
|
fieldName: 'file',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
customUpload: async (file: File, insertFn: (url: string, poster?: string) => void) => {
|
||||||
|
await handleUploadVideo(file, insertFn);
|
||||||
|
},
|
||||||
|
allowedFileTypes: ['video/*'],
|
||||||
|
maxFileSize: 100 * 1024 * 1024, // 100MB
|
||||||
|
},
|
||||||
|
// 附件上传配置
|
||||||
|
uploadAttachment: {
|
||||||
|
server: '/api/files',
|
||||||
|
fieldName: 'file',
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
customUpload: async (file: File, insertFn: (url: string, text?: string) => void) => {
|
||||||
|
await handleUploadAttachment(file, insertFn);
|
||||||
|
},
|
||||||
|
allowedFileTypes: ['*'],
|
||||||
|
maxFileSize: 50 * 1024 * 1024, // 50MB
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建编辑器
|
// 创建编辑器
|
||||||
|
|||||||
@ -43,7 +43,7 @@
|
|||||||
<span>{{ formData.updateTime }}</span>
|
<span>{{ formData.updateTime }}</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<el-form label-width="80px" size="small" align="center">
|
<el-form label-width="80px" align="center">
|
||||||
<el-divider />
|
<el-divider />
|
||||||
<el-button type="primary" @click="handleEdit"
|
<el-button type="primary" @click="handleEdit"
|
||||||
><i class="fas fa-edit"></i> 编辑</el-button
|
><i class="fas fa-edit"></i> 编辑</el-button
|
||||||
|
|||||||
@ -160,11 +160,12 @@
|
|||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
<div class="empty-state" v-if="repoList.length === 0">
|
<div class="empty-state" v-if="repoList.length === 0">
|
||||||
<div class="empty-icon">
|
<div class="empty-icon">
|
||||||
<i class="el-icon-folder-opened"></i>
|
<i class="fa-solid fa-folder-open"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3>暂无知识库</h3>
|
<h3>暂无知识库</h3>
|
||||||
<p>点击下方按钮创建您的第一个知识库</p>
|
<p>点击下方按钮创建您的第一个知识库</p>
|
||||||
<el-button type="primary" icon="el-icon-plus" @click="handleCreate">
|
<el-button type="primary" @click="handleCreate">
|
||||||
|
<i class="fa-solid fa-plus"></i>
|
||||||
新建知识库
|
新建知识库
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -227,6 +227,7 @@
|
|||||||
drag
|
drag
|
||||||
:action="uploadUrl"
|
:action="uploadUrl"
|
||||||
:headers="uploadHeaders"
|
:headers="uploadHeaders"
|
||||||
|
:data="{ category: uploadForm.category }"
|
||||||
:on-success="handleUploadSuccess"
|
:on-success="handleUploadSuccess"
|
||||||
:on-error="handleUploadError"
|
:on-error="handleUploadError"
|
||||||
:before-upload="beforeUpload"
|
:before-upload="beforeUpload"
|
||||||
@ -360,8 +361,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from "vue";
|
import { ref, computed, onMounted } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
import {
|
import {
|
||||||
Download,
|
Download,
|
||||||
Delete,
|
Delete,
|
||||||
@ -369,6 +371,7 @@ import {
|
|||||||
Document,
|
Document,
|
||||||
View,
|
View,
|
||||||
Link,
|
Link,
|
||||||
|
UploadFilled,
|
||||||
} from "@element-plus/icons-vue";
|
} from "@element-plus/icons-vue";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -429,8 +432,20 @@ const pageSize = ref(10);
|
|||||||
const currentPage = ref(1);
|
const currentPage = ref(1);
|
||||||
const totalFiles = ref(0);
|
const totalFiles = ref(0);
|
||||||
const showUploadDialog = ref(false);
|
const showUploadDialog = ref(false);
|
||||||
const uploadUrl = "";
|
|
||||||
const uploadHeaders = {};
|
// 上传配置
|
||||||
|
const uploadUrl = computed(() => {
|
||||||
|
const baseUrl = import.meta.env.VITE_API_BASE_URL ;
|
||||||
|
return `${baseUrl}/api/files`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const uploadHeaders = computed(() => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
return {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const uploadForm = ref({
|
const uploadForm = ref({
|
||||||
category: "",
|
category: "",
|
||||||
isPublic: false,
|
isPublic: false,
|
||||||
@ -442,11 +457,37 @@ const handleSearch = () => {};
|
|||||||
const handleFilter = () => {};
|
const handleFilter = () => {};
|
||||||
const handleSizeChange = () => {};
|
const handleSizeChange = () => {};
|
||||||
const handleCurrentChange = () => {};
|
const handleCurrentChange = () => {};
|
||||||
const handleUploadClose = () => {};
|
const handleUploadClose = () => {
|
||||||
const handleUploadSuccess = () => {};
|
showUploadDialog.value = false;
|
||||||
const handleUploadError = () => {};
|
uploadForm.value.category = "";
|
||||||
const beforeUpload = () => {};
|
uploadForm.value.isPublic = false;
|
||||||
const submitUpload = () => {};
|
};
|
||||||
|
|
||||||
|
const handleUploadSuccess = (response: any, file: any) => {
|
||||||
|
ElMessage.success('文件上传成功!');
|
||||||
|
// 关闭对话框
|
||||||
|
showUploadDialog.value = false;
|
||||||
|
// 可以刷新文件列表
|
||||||
|
// loadFiles();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUploadError = (error: Error, file: any) => {
|
||||||
|
ElMessage.error('文件上传失败:' + error.message);
|
||||||
|
};
|
||||||
|
|
||||||
|
const beforeUpload = (file: File) => {
|
||||||
|
const maxSize = 10 * 1024 * 1024; // 10MB
|
||||||
|
if (file.size > maxSize) {
|
||||||
|
ElMessage.error('文件大小不能超过 10MB!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitUpload = () => {
|
||||||
|
// Element Plus 的 el-upload 组件会自动处理上传
|
||||||
|
ElMessage.info('开始上传文件...');
|
||||||
|
};
|
||||||
const viewFile = (row: any) => {};
|
const viewFile = (row: any) => {};
|
||||||
const formatDate = (val: string | number) => val;
|
const formatDate = (val: string | number) => val;
|
||||||
const copyFileUrl = (file: any) => {};
|
const copyFileUrl = (file: any) => {};
|
||||||
|
|||||||
@ -155,7 +155,7 @@ const uploadForm = ref({
|
|||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const uploadUrl = computed(() => {
|
const uploadUrl = computed(() => {
|
||||||
const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'
|
const baseUrl = import.meta.env.VITE_API_BASE_URL
|
||||||
return `${baseUrl}/api/files`
|
return `${baseUrl}/api/files`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -15,5 +15,6 @@ mysqldb = gotest
|
|||||||
# ORM配置
|
# ORM配置
|
||||||
orm = mysql
|
orm = mysql
|
||||||
|
|
||||||
# 配置静态文件目录(指向前端 dist 目录)
|
# 配置静态文件目录
|
||||||
StaticDir = /static:../front/dist # 映射 /static 路径到前端 dist 目录
|
StaticDir = /static:../front/dist # 映射 /static 路径到前端 dist 目录
|
||||||
|
StaticDir = /uploads:../uploads # 映射 /uploads 路径到上传文件目录(项目根目录下的 uploads 文件夹)
|
||||||
@ -2,8 +2,14 @@ package controllers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"strconv"
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"server/models"
|
"server/models"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
beego "github.com/beego/beego/v2/server/web"
|
beego "github.com/beego/beego/v2/server/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -178,8 +184,8 @@ func (c *FileController) CreateFile() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 验证必填字段
|
// 验证必填字段
|
||||||
if file.TenantID == "" || file.FileName == "" || file.OriginalName == "" ||
|
if file.TenantID == "" || file.FileName == "" || file.OriginalName == "" ||
|
||||||
file.FilePath == "" || file.FileType == "" || file.FileExt == "" ||
|
file.FilePath == "" || file.FileType == "" || file.FileExt == "" ||
|
||||||
file.Category == "" || file.UploadBy == "" {
|
file.Category == "" || file.UploadBy == "" {
|
||||||
c.Data["json"] = map[string]interface{}{
|
c.Data["json"] = map[string]interface{}{
|
||||||
"success": false,
|
"success": false,
|
||||||
@ -344,7 +350,7 @@ func (c *FileController) GetFileStatistics() {
|
|||||||
func (c *FileController) SearchFiles() {
|
func (c *FileController) SearchFiles() {
|
||||||
keyword := c.GetString("keyword")
|
keyword := c.GetString("keyword")
|
||||||
tenantID := c.GetString("tenant_id")
|
tenantID := c.GetString("tenant_id")
|
||||||
|
|
||||||
if keyword == "" {
|
if keyword == "" {
|
||||||
c.Data["json"] = map[string]interface{}{
|
c.Data["json"] = map[string]interface{}{
|
||||||
"success": false,
|
"success": false,
|
||||||
@ -353,7 +359,7 @@ func (c *FileController) SearchFiles() {
|
|||||||
c.ServeJSON()
|
c.ServeJSON()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if tenantID == "" {
|
if tenantID == "" {
|
||||||
c.Data["json"] = map[string]interface{}{
|
c.Data["json"] = map[string]interface{}{
|
||||||
"success": false,
|
"success": false,
|
||||||
@ -404,7 +410,7 @@ func (c *FileController) GetMyFiles() {
|
|||||||
c.Data["json"] = map[string]interface{}{
|
c.Data["json"] = map[string]interface{}{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "获取成功",
|
"message": "获取成功",
|
||||||
"data": files,
|
"data": files,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.ServeJSON()
|
c.ServeJSON()
|
||||||
@ -429,4 +435,148 @@ func (c *FileController) GetFilesPublic() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.ServeJSON()
|
c.ServeJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Post 处理文件上传
|
||||||
|
func (c *FileController) Post() {
|
||||||
|
// 从JWT中间件获取用户信息
|
||||||
|
userID, ok := c.Ctx.Input.GetData("userId").(int)
|
||||||
|
if !ok || userID <= 0 {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"message": "用户未登录或登录已过期",
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
username, _ := c.Ctx.Input.GetData("username").(string)
|
||||||
|
if username == "" {
|
||||||
|
username = "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取上传的文件
|
||||||
|
file, header, err := c.GetFile("file")
|
||||||
|
if err != nil {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"message": "获取上传文件失败: " + err.Error(),
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// 获取文件基本信息
|
||||||
|
originalName := header.Filename
|
||||||
|
fileSize := header.Size
|
||||||
|
fileExt := strings.ToLower(filepath.Ext(originalName))
|
||||||
|
fileName := strings.TrimSuffix(originalName, fileExt)
|
||||||
|
|
||||||
|
// 获取分类(可选)
|
||||||
|
category := c.GetString("category")
|
||||||
|
if category == "" {
|
||||||
|
category = "未分类"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取租户ID(可选,从请求参数或中间件获取)
|
||||||
|
tenantID := c.GetString("tenant_id")
|
||||||
|
if tenantID == "" {
|
||||||
|
tenantID = "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成日期路径(年/月/日)
|
||||||
|
now := time.Now()
|
||||||
|
datePath := now.Format("2006/01/02")
|
||||||
|
|
||||||
|
// 目录改成 server 文件夹目录下的 uploads
|
||||||
|
uploadDir := filepath.Join("uploads", datePath)
|
||||||
|
|
||||||
|
// 清理路径
|
||||||
|
uploadDir = filepath.Clean(uploadDir)
|
||||||
|
|
||||||
|
// 确保目录存在
|
||||||
|
if err := os.MkdirAll(uploadDir, 0755); err != nil {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"message": "创建上传目录失败: " + err.Error(),
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成唯一文件名(时间戳 + 原始文件名)
|
||||||
|
timestamp := now.Format("20060102150405")
|
||||||
|
uniqueFileName := timestamp + "_" + originalName
|
||||||
|
savePath := path.Join(uploadDir, uniqueFileName)
|
||||||
|
|
||||||
|
// 计算相对路径(用于存储到数据库)
|
||||||
|
relativePath := path.Join("uploads", datePath, uniqueFileName)
|
||||||
|
|
||||||
|
// 保存文件
|
||||||
|
if err := c.SaveToFile("file", savePath); err != nil {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"message": "保存文件失败: " + err.Error(),
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件类型
|
||||||
|
fileType := "other"
|
||||||
|
switch fileExt {
|
||||||
|
case ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp":
|
||||||
|
fileType = "image"
|
||||||
|
case ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".txt":
|
||||||
|
fileType = "document"
|
||||||
|
case ".mp4", ".avi", ".mov", ".wmv":
|
||||||
|
fileType = "video"
|
||||||
|
case ".mp3", ".wav", ".flac":
|
||||||
|
fileType = "audio"
|
||||||
|
case ".zip", ".rar", ".7z":
|
||||||
|
fileType = "archive"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造文件URL(相对路径)
|
||||||
|
fileURL := "/" + relativePath
|
||||||
|
|
||||||
|
// 创建文件信息记录
|
||||||
|
fileInfo := models.FileInfo{
|
||||||
|
TenantID: tenantID,
|
||||||
|
UserID: userID,
|
||||||
|
FileName: fileName,
|
||||||
|
OriginalName: originalName,
|
||||||
|
FilePath: relativePath,
|
||||||
|
FileURL: fileURL,
|
||||||
|
FileSize: fileSize,
|
||||||
|
FileType: fileType,
|
||||||
|
FileExt: fileExt,
|
||||||
|
Category: category,
|
||||||
|
UploadBy: username,
|
||||||
|
UploadTime: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到数据库
|
||||||
|
id, err := models.AddFile(&fileInfo)
|
||||||
|
if err != nil {
|
||||||
|
// 如果数据库保存失败,删除已上传的文件
|
||||||
|
os.Remove(savePath)
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"message": "保存文件信息失败: " + err.Error(),
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fileInfo.ID = id
|
||||||
|
|
||||||
|
// 返回成功响应
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"success": true,
|
||||||
|
"message": "文件上传成功",
|
||||||
|
"data": fileInfo,
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
}
|
||||||
|
|||||||
203
server/docs/FILE_UPLOAD.md
Normal file
203
server/docs/FILE_UPLOAD.md
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
# 文件上传功能文档
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
文件上传功能已完整实现,支持将文件保存到本地文件系统并记录到数据库中。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
1. **自动目录管理**:按年月日自动创建目录结构(如 `front/uploads/2024/01/15/`)
|
||||||
|
2. **唯一文件名**:使用时间戳生成唯一文件名,避免重名冲突
|
||||||
|
3. **文件类型识别**:自动识别文件类型(图片、文档、视频、音频、压缩包等)
|
||||||
|
4. **数据库记录**:所有文件信息保存到 `yz_files` 表
|
||||||
|
5. **用户关联**:自动关联当前登录用户
|
||||||
|
6. **异常处理**:文件保存失败时自动清理已上传的文件
|
||||||
|
|
||||||
|
## 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
front/
|
||||||
|
└── uploads/
|
||||||
|
├── 2024/
|
||||||
|
│ ├── 01/
|
||||||
|
│ │ ├── 15/
|
||||||
|
│ │ │ ├── 20240115143045_example.jpg
|
||||||
|
│ │ │ └── 20240115143046_document.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 接口
|
||||||
|
|
||||||
|
### 上传文件
|
||||||
|
|
||||||
|
**POST** `/api/files`
|
||||||
|
|
||||||
|
**请求头**:
|
||||||
|
```
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
- `file` (File, required): 上传的文件
|
||||||
|
- `category` (String, optional): 文件分类,默认为"未分类"
|
||||||
|
- `tenant_id` (String, optional): 租户ID,默认为"default"
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "文件上传成功",
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"tenant_id": "default",
|
||||||
|
"user_id": 123,
|
||||||
|
"file_name": "example",
|
||||||
|
"original_name": "example.jpg",
|
||||||
|
"file_path": "uploads/2024/01/15/20240115143045_example.jpg",
|
||||||
|
"file_url": "/uploads/2024/01/15/20240115143045_example.jpg",
|
||||||
|
"file_size": 102400,
|
||||||
|
"file_type": "image",
|
||||||
|
"file_ext": ".jpg",
|
||||||
|
"category": "未分类",
|
||||||
|
"upload_by": "username",
|
||||||
|
"upload_time": "2024-01-15T14:30:45Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 后端实现
|
||||||
|
|
||||||
|
### 路由配置
|
||||||
|
|
||||||
|
在 `server/routers/router.go` 中配置:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 文件管理路由
|
||||||
|
beego.Router("/api/files", &controllers.FileController{}, "get:GetAllFiles")
|
||||||
|
beego.Router("/api/files", &controllers.FileController{}, "post:Post")
|
||||||
|
beego.Router("/api/files/my", &controllers.FileController{}, "get:GetMyFiles")
|
||||||
|
beego.Router("/api/files/:id", &controllers.FileController{}, "get:GetFileById")
|
||||||
|
beego.Router("/api/files/:id", &controllers.FileController{}, "put:UpdateFile")
|
||||||
|
beego.Router("/api/files/:id", &controllers.FileController{}, "delete:DeleteFile")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 控制器实现
|
||||||
|
|
||||||
|
`Post()` 方法位于 `server/controllers/file.go`:
|
||||||
|
|
||||||
|
1. 验证用户登录状态
|
||||||
|
2. 接收上传的文件
|
||||||
|
3. 生成日期路径和唯一文件名
|
||||||
|
4. 保存文件到本地
|
||||||
|
5. 记录文件信息到数据库
|
||||||
|
6. 返回文件信息
|
||||||
|
|
||||||
|
## 前端使用
|
||||||
|
|
||||||
|
### 基本用法
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { fileAPI } from '@/api/file'
|
||||||
|
|
||||||
|
// 创建 FormData
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', fileObject) // fileObject 是 File 对象
|
||||||
|
formData.append('category', '文档')
|
||||||
|
formData.append('tenant_id', 'tenant-001')
|
||||||
|
|
||||||
|
// 上传文件
|
||||||
|
try {
|
||||||
|
const response = await fileAPI.uploadFile(formData, {
|
||||||
|
category: '文档',
|
||||||
|
tenantId: 'tenant-001'
|
||||||
|
})
|
||||||
|
console.log('上传成功:', response.data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传失败:', error)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element Plus 上传组件
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<el-upload
|
||||||
|
ref="uploadRef"
|
||||||
|
drag
|
||||||
|
:action="uploadUrl"
|
||||||
|
:headers="uploadHeaders"
|
||||||
|
:data="{ category: uploadForm.category }"
|
||||||
|
:on-success="handleUploadSuccess"
|
||||||
|
:on-error="handleUploadError"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
multiple
|
||||||
|
>
|
||||||
|
<el-icon><upload-filled /></el-icon>
|
||||||
|
<div class="el-upload__text">
|
||||||
|
将文件拖到此处,或<em>点击上传</em>
|
||||||
|
</div>
|
||||||
|
</el-upload>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const uploadUrl = computed(() => {
|
||||||
|
const baseUrl = import.meta.env.VITE_API_BASE_URL
|
||||||
|
return `${baseUrl}/api/files`
|
||||||
|
})
|
||||||
|
|
||||||
|
const uploadHeaders = computed(() => {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
return {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleUploadSuccess = (response) => {
|
||||||
|
console.log('上传成功:', response)
|
||||||
|
}
|
||||||
|
|
||||||
|
const beforeUpload = (file) => {
|
||||||
|
const maxSize = 10 * 1024 * 1024 // 10MB
|
||||||
|
if (file.size > maxSize) {
|
||||||
|
ElMessage.error('文件大小不能超过 10MB!')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 文件访问
|
||||||
|
|
||||||
|
上传后的文件可以通过以下URL访问:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost:8080/uploads/2024/01/15/20240115143045_example.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:需要配置 Beego 的静态文件服务来提供上传文件的访问。
|
||||||
|
|
||||||
|
在 `server/conf/app.conf` 中添加:
|
||||||
|
|
||||||
|
```conf
|
||||||
|
# 文件上传目录
|
||||||
|
StaticDir = /uploads:../front/uploads
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安全注意事项
|
||||||
|
|
||||||
|
1. **文件大小限制**:建议在前端和后端都添加文件大小限制
|
||||||
|
2. **文件类型验证**:根据业务需求限制允许上传的文件类型
|
||||||
|
3. **文件名安全**:避免用户控制文件名造成安全问题
|
||||||
|
4. **权限控制**:确保只有授权用户可以上传文件
|
||||||
|
5. **存储位置**:考虑使用对象存储服务(如 OSS、S3)替代本地存储
|
||||||
|
|
||||||
|
## 数据库字段说明
|
||||||
|
|
||||||
|
`yz_files` 表的主要字段:
|
||||||
|
|
||||||
|
- `file_path`: 相对路径,用于存储和访问文件
|
||||||
|
- `file_url`: 访问URL
|
||||||
|
- `original_name`: 用户上传时的原始文件名
|
||||||
|
- `file_name`: 去除扩展名的文件名
|
||||||
|
- `file_type`: 文件类型(image, document, video, audio, archive, other)
|
||||||
|
- `category`: 用户自定义分类
|
||||||
|
|
||||||
164
server/docs/UPLOAD_PATH.md
Normal file
164
server/docs/UPLOAD_PATH.md
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
# 文件上传路径说明
|
||||||
|
|
||||||
|
## 当前配置
|
||||||
|
|
||||||
|
### 文件保存路径
|
||||||
|
文件保存在项目根目录的 `front/uploads/` 目录下,按照日期自动分类:
|
||||||
|
|
||||||
|
```
|
||||||
|
项目根目录/
|
||||||
|
├── server/
|
||||||
|
│ └── controllers/
|
||||||
|
│ └── file.go (处理上传)
|
||||||
|
├── front/
|
||||||
|
│ └── uploads/ ← 文件保存位置
|
||||||
|
│ ├── 2024/
|
||||||
|
│ │ └── 01/
|
||||||
|
│ │ └── 15/
|
||||||
|
│ │ └── 20240115143045_example.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
### 代码中的路径
|
||||||
|
|
||||||
|
在 `server/controllers/file.go` 的 `Post()` 方法中:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 构造保存路径:../front/uploads/年/月/日/
|
||||||
|
uploadDir := path.Join("..", "front", "uploads", datePath)
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明**:
|
||||||
|
- `..` 表示从 server 目录向上一级到项目根目录
|
||||||
|
- `front/uploads/` 是上传文件的根目录
|
||||||
|
- `datePath` 是按日期自动生成的子目录(如 `2024/01/15`)
|
||||||
|
|
||||||
|
### 静态文件访问配置
|
||||||
|
|
||||||
|
在 `server/conf/app.conf` 中:
|
||||||
|
|
||||||
|
```conf
|
||||||
|
StaticDir = /uploads:../front/uploads
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明**:
|
||||||
|
- `/uploads` 是 URL 访问路径
|
||||||
|
- `../front/uploads` 是实际文件存储路径(相对于 server 目录)
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
### 保存到数据库的路径
|
||||||
|
- `file_path`: `uploads/2024/01/15/20240115143045_example.jpg`(相对路径)
|
||||||
|
- `file_url`: `/uploads/2024/01/15/20240115143045_example.jpg`(URL 路径)
|
||||||
|
|
||||||
|
### 实际文件系统路径
|
||||||
|
```
|
||||||
|
front/uploads/2024/01/15/20240115143045_example.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
### 访问 URL
|
||||||
|
```
|
||||||
|
http://localhost:8080/uploads/2024/01/15/20240115143045_example.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
## 为什么使用相对路径 `..`
|
||||||
|
|
||||||
|
由于项目结构是:
|
||||||
|
```
|
||||||
|
yunzer_go/
|
||||||
|
├── server/ ← 服务端代码
|
||||||
|
└── front/ ← 前端代码和上传文件
|
||||||
|
└── uploads/
|
||||||
|
```
|
||||||
|
|
||||||
|
从 `server` 目录运行应用时,要访问 `front/uploads`,需要使用 `../front/uploads`。
|
||||||
|
|
||||||
|
## 如果路径不对怎么办?
|
||||||
|
|
||||||
|
### 方案1:修改代码中的路径
|
||||||
|
如果您的项目启动目录不同,可以修改 `file.go` 中的路径:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 如果从项目根目录运行
|
||||||
|
uploadDir := path.Join("front", "uploads", datePath)
|
||||||
|
|
||||||
|
// 或者使用绝对路径
|
||||||
|
uploadDir := path.Join("/path/to/project", "front", "uploads", datePath)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方案2:从配置文件读取
|
||||||
|
在 `app.conf` 中添加配置:
|
||||||
|
|
||||||
|
```conf
|
||||||
|
# 上传文件目录
|
||||||
|
uploadDir = ../front/uploads
|
||||||
|
```
|
||||||
|
|
||||||
|
然后在代码中读取:
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/beego/beego/v2/server/web"
|
||||||
|
|
||||||
|
uploadDir := path.Join(
|
||||||
|
web.AppConfig.String("uploadDir"),
|
||||||
|
datePath,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 验证路径是否正确
|
||||||
|
|
||||||
|
### 1. 检查文件保存位置
|
||||||
|
上传一个文件后,查看文件是否在正确的位置:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls front/uploads/
|
||||||
|
```
|
||||||
|
|
||||||
|
应该看到按日期分类的文件夹和文件。
|
||||||
|
|
||||||
|
### 2. 检查数据库记录
|
||||||
|
查看 `yz_files` 表中的 `file_path` 字段:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT file_path, file_url FROM yz_files ORDER BY upload_time DESC LIMIT 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
应该看到类似:
|
||||||
|
```
|
||||||
|
file_path: uploads/2024/01/15/20240115143045_example.jpg
|
||||||
|
file_url: /uploads/2024/01/15/20240115143045_example.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 检查 URL 访问
|
||||||
|
直接在浏览器访问:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost:8080/uploads/2024/01/15/文件名
|
||||||
|
```
|
||||||
|
|
||||||
|
如果能看到文件,说明路径配置正确。
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q: 文件保存在了 server/front/uploads?
|
||||||
|
A: 修改代码中的路径为 `../front/uploads`(已经修改)
|
||||||
|
|
||||||
|
### Q: 文件保存在了 front/front/uploads?
|
||||||
|
A: 检查当前工作目录,确保在 server 目录运行应用
|
||||||
|
|
||||||
|
### Q: 访问文件返回 404?
|
||||||
|
A: 检查 `app.conf` 中的 StaticDir 配置是否正确
|
||||||
|
|
||||||
|
### Q: 权限问题?
|
||||||
|
A: 确保应用有创建目录和写入文件的权限:
|
||||||
|
```bash
|
||||||
|
chmod 755 front/uploads
|
||||||
|
```
|
||||||
|
|
||||||
|
## 建议的改进
|
||||||
|
|
||||||
|
如果需要更可靠的路径处理,可以考虑:
|
||||||
|
|
||||||
|
1. **使用绝对路径**:从配置文件或环境变量读取项目根目录
|
||||||
|
2. **路径验证**:在应用启动时检查上传目录是否存在,不存在则创建
|
||||||
|
3. **日志记录**:记录文件保存的完整路径,便于调试
|
||||||
|
|
||||||
@ -79,8 +79,15 @@ func init() {
|
|||||||
// 程序信息路由 - 自动映射到 /api/programinfo/*
|
// 程序信息路由 - 自动映射到 /api/programinfo/*
|
||||||
beego.AutoRouter(&controllers.ProgramInfoController{})
|
beego.AutoRouter(&controllers.ProgramInfoController{})
|
||||||
|
|
||||||
// 文件管理路由 - 自动映射到 /api/file/*
|
// 文件管理路由 - 手动配置以匹配前端的 /api/files 路径
|
||||||
beego.AutoRouter(&controllers.FileController{})
|
beego.Router("/api/files", &controllers.FileController{}, "get:GetAllFiles")
|
||||||
|
beego.Router("/api/files", &controllers.FileController{}, "post:Post")
|
||||||
|
beego.Router("/api/files/my", &controllers.FileController{}, "get:GetMyFiles")
|
||||||
|
beego.Router("/api/files/:id", &controllers.FileController{}, "get:GetFileById")
|
||||||
|
beego.Router("/api/files/:id", &controllers.FileController{}, "put:UpdateFile")
|
||||||
|
beego.Router("/api/files/:id", &controllers.FileController{}, "delete:DeleteFile")
|
||||||
|
beego.Router("/api/files/search", &controllers.FileController{}, "get:SearchFiles")
|
||||||
|
beego.Router("/api/files/statistics", &controllers.FileController{}, "get:GetFileStatistics")
|
||||||
|
|
||||||
// 知识库路由
|
// 知识库路由
|
||||||
beego.Router("/api/knowledge/list", &controllers.KnowledgeController{}, "get:List")
|
beego.Router("/api/knowledge/list", &controllers.KnowledgeController{}, "get:List")
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 129 KiB |
1
文件上传功能实现总结.md
Normal file
1
文件上传功能实现总结.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user