实现上传功能

This commit is contained in:
2025-10-28 17:22:27 +08:00
parent 1062bcfb70
commit 74df0e539c
15 changed files with 1126 additions and 24 deletions
+8 -1
View File
@@ -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, {
headers: {
'Content-Type': 'multipart/form-data'
+204
View 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. 添加文件管理器功能
+151
View File
@@ -7,7 +7,9 @@
<script setup lang="ts">
import { ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { ElMessage } from 'element-plus';
import '@wangeditor/editor/dist/css/style.css';
import { fileAPI } from '@/api/file';
interface Props {
modelValue: string;
@@ -26,6 +28,119 @@ const editorRef = ref<HTMLDivElement>();
let editorInstance: any = null;
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 () => {
if (!editorRef.value || !toolbarRef.value || isDestroyed) return;
@@ -42,6 +157,42 @@ const initEditor = async () => {
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>
</el-form-item>
</el-form>
<el-form label-width="80px" size="small" align="center">
<el-form label-width="80px" align="center">
<el-divider />
<el-button type="primary" @click="handleEdit"
><i class="fas fa-edit"></i> 编辑</el-button
+3 -2
View File
@@ -160,11 +160,12 @@
<!-- 空状态 -->
<div class="empty-state" v-if="repoList.length === 0">
<div class="empty-icon">
<i class="el-icon-folder-opened"></i>
<i class="fa-solid fa-folder-open"></i>
</div>
<h3>暂无知识库</h3>
<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>
</div>
+49 -8
View File
@@ -227,6 +227,7 @@
drag
:action="uploadUrl"
:headers="uploadHeaders"
:data="{ category: uploadForm.category }"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:before-upload="beforeUpload"
@@ -360,8 +361,9 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { ref, computed, onMounted } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import {
Download,
Delete,
@@ -369,6 +371,7 @@ import {
Document,
View,
Link,
UploadFilled,
} from "@element-plus/icons-vue";
const router = useRouter();
@@ -429,8 +432,20 @@ const pageSize = ref(10);
const currentPage = ref(1);
const totalFiles = ref(0);
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({
category: "",
isPublic: false,
@@ -442,11 +457,37 @@ const handleSearch = () => {};
const handleFilter = () => {};
const handleSizeChange = () => {};
const handleCurrentChange = () => {};
const handleUploadClose = () => {};
const handleUploadSuccess = () => {};
const handleUploadError = () => {};
const beforeUpload = () => {};
const submitUpload = () => {};
const handleUploadClose = () => {
showUploadDialog.value = false;
uploadForm.value.category = "";
uploadForm.value.isPublic = false;
};
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 formatDate = (val: string | number) => val;
const copyFileUrl = (file: any) => {};
+1 -1
View File
@@ -155,7 +155,7 @@ const uploadForm = ref({
// 计算属性
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`
})