实现上传功能
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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">
|
||||
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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {};
|
||||
|
||||
@@ -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`
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user