前端逻辑更新
This commit is contained in:
parent
a1e5554823
commit
9b1a2f51e8
41
minio-admin/src/main/java/com/mmg/types/JsonResponse.java
Normal file
41
minio-admin/src/main/java/com/mmg/types/JsonResponse.java
Normal file
@ -0,0 +1,41 @@
|
||||
package com.mmg.types;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class JsonResponse {
|
||||
|
||||
private Integer code;
|
||||
private String msg;
|
||||
private Object data;
|
||||
|
||||
public JsonResponse(Integer code, String msg, Object data) {
|
||||
this.code = code;
|
||||
this.msg = msg;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public static JsonResponse success(String msg) {
|
||||
return new JsonResponse(0, msg, null);
|
||||
}
|
||||
|
||||
public static JsonResponse success() {
|
||||
return new JsonResponse(0, "", null);
|
||||
}
|
||||
|
||||
public static JsonResponse data(Object data) {
|
||||
return new JsonResponse(0, "", data);
|
||||
}
|
||||
|
||||
public static JsonResponse error(String msg, Integer code) {
|
||||
return new JsonResponse(code, msg, null);
|
||||
}
|
||||
|
||||
public static JsonResponse error(String msg) {
|
||||
return new JsonResponse(-1, msg, null);
|
||||
}
|
||||
|
||||
public static JsonResponse error(String msg, Object data) {
|
||||
return new JsonResponse(-1, msg, data);
|
||||
}
|
||||
}
|
@ -7,4 +7,10 @@ import {RouterLink, RouterView} from 'vue-router'
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
body {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
17
minio-fornt/src/api/resource.js
Normal file
17
minio-fornt/src/api/resource.js
Normal file
@ -0,0 +1,17 @@
|
||||
// src/api/resource.js
|
||||
import axios from '../utils/request';
|
||||
|
||||
export const resource = {
|
||||
resourceList: (page, size, title, type, categoryIds) =>
|
||||
axios.get('/resource/list', {
|
||||
params: { page, size, title, type, categoryIds },
|
||||
}),
|
||||
|
||||
destroyResource: (id) => axios.delete(`/resource/${id}`),
|
||||
|
||||
destroyResourceMulti: (ids) => axios.post('/resource/batch-delete', { ids }),
|
||||
|
||||
videoDetail: (id) => axios.get(`/resource/${id}`),
|
||||
|
||||
videoUpdate: (id, data) => axios.put(`/resource/${id}`, data),
|
||||
};
|
6
minio-fornt/src/api/resourceCategory.js
Normal file
6
minio-fornt/src/api/resourceCategory.js
Normal file
@ -0,0 +1,6 @@
|
||||
// src/api/resourceCategory.js
|
||||
import axios from '../utils/request';
|
||||
|
||||
export const resourceCategory = {
|
||||
resourceCategoryList: () => axios.get('/resource-category/list'),
|
||||
};
|
@ -1,49 +1,66 @@
|
||||
import request from '@/utils/request'
|
||||
// src/api/upload.js
|
||||
import axios from '../utils/request';
|
||||
|
||||
//上传信息
|
||||
export function uploadScreenshot(data){
|
||||
return request({
|
||||
url:'upload/multipart/uploadScreenshot',
|
||||
method:'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
//上传信息
|
||||
export function uploadFileInfo(data){
|
||||
return request({
|
||||
url:'upload/multipart/uploadFileInfo',
|
||||
method:'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 上传校验
|
||||
export function checkUpload(MD5) {
|
||||
return request({
|
||||
url: `upload/multipart/check?md5=${MD5}`,
|
||||
method: 'get',
|
||||
})
|
||||
/**
|
||||
* 获取上传中的文件信息
|
||||
* @param {string} fileMD5 - 文件的 MD5 值
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const getUploadingFile = (fileMD5) => {
|
||||
return axios.get(`/upload/getUploadingFile/${fileMD5}`);
|
||||
};
|
||||
|
||||
|
||||
// 初始化上传
|
||||
export function initUpload(data) {
|
||||
return request({
|
||||
url: `upload/multipart/init`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
/**
|
||||
* 校验文件是否已经上传
|
||||
* @param {string} md5 - 文件的 MD5 值
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const checkFileUploadedByMd5 = (md5) => {
|
||||
return axios.get('/upload/multipart/check', {
|
||||
params: { md5 },
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// 初始化上传
|
||||
export function mergeUpload(data) {
|
||||
return request({
|
||||
url: `upload/multipart/merge`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
/**
|
||||
* 初始化分片上传
|
||||
* @param {Object} fileUploadInfo - 文件上传信息
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const initMultiPartUpload = (fileUploadInfo) => {
|
||||
return axios.post('/upload/multipart/init', fileUploadInfo);
|
||||
};
|
||||
|
||||
/**
|
||||
* 完成分片上传
|
||||
* @param {Object} fileUploadInfo - 文件上传信息
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const completeMultiPartUpload = (fileUploadInfo) => {
|
||||
return axios.post('/upload/multipart/merge', fileUploadInfo);
|
||||
};
|
||||
|
||||
/**
|
||||
* 上传截图
|
||||
* @param {FormData} formData - 包含截图文件的 FormData
|
||||
* @param {string} bucketName - Bucket 名称
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const uploadScreenshot = (formData, bucketName) => {
|
||||
return axios.post('/upload/multipart/uploadScreenshot', formData, {
|
||||
params: { bucketName },
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建 Bucket
|
||||
* @param {string} bucketName - Bucket 名称
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const createBucket = (bucketName) => {
|
||||
return axios.post('/upload/createBucket', null, {
|
||||
params: { bucketName },
|
||||
});
|
||||
};
|
||||
|
260
minio-fornt/src/components/Resource/FileUploader.vue
Normal file
260
minio-fornt/src/components/Resource/FileUploader.vue
Normal file
@ -0,0 +1,260 @@
|
||||
<template>
|
||||
<div class="file-uploader">
|
||||
<el-upload
|
||||
ref="upload"
|
||||
:file-list="fileList"
|
||||
:before-upload="beforeUpload"
|
||||
:auto-upload="false"
|
||||
multiple
|
||||
drag
|
||||
accept=".doc,.docx,.ppt,.pptx,.pdf,.txt,.rar,.zip,.jpg,.jpeg,.png"
|
||||
@change="handleChange"
|
||||
>
|
||||
<i class="el-icon-upload"></i>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
<div slot="tip" class="el-upload__tip">支持上传的格式: DOC, DOCX, PPT, PPTX, PDF, TXT, RAR, ZIP</div>
|
||||
</el-upload>
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="!fileList.length"
|
||||
@click="uploadFiles"
|
||||
class="upload-button"
|
||||
>
|
||||
上传
|
||||
</el-button>
|
||||
|
||||
<el-progress v-if="uploadProgress > 0" :percentage="uploadProgress" class="upload-progress" />
|
||||
|
||||
<div v-if="uploadResult" class="upload-result">
|
||||
<h3>上传结果</h3>
|
||||
<p>
|
||||
文件 URL:
|
||||
<a :href="uploadResult.url" target="_blank">{{ uploadResult.url }}</a>
|
||||
</p>
|
||||
<p>文件名: {{ uploadResult.resource.originalName }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="errorMessage" class="error-message">
|
||||
<el-alert :title="errorMessage" type="error" show-icon />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import {
|
||||
checkFileUploadedByMd5,
|
||||
getUploadingFile,
|
||||
initMultiPartUpload,
|
||||
completeMultiPartUpload,
|
||||
} from '../../api/upload.js';
|
||||
import SparkMD5 from 'spark-md5';
|
||||
|
||||
interface FileUploadInfo {
|
||||
adminId?: string;
|
||||
categoryIds?: string[];
|
||||
resourceType: string;
|
||||
originalName: string;
|
||||
extension: string;
|
||||
size: number;
|
||||
savePath: string;
|
||||
uploadId?: string;
|
||||
}
|
||||
|
||||
interface Resource {
|
||||
id: string;
|
||||
originalName: string;
|
||||
extension: string;
|
||||
size: number;
|
||||
md5: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface UploadResult {
|
||||
url: string;
|
||||
resource: Resource;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FileUploader',
|
||||
emits: ['upload-success'],
|
||||
setup(props, { emit }) {
|
||||
const fileList = ref<any[]>([]);
|
||||
const uploadProgress = ref<number>(0);
|
||||
const uploadResult = ref<UploadResult | null>(null);
|
||||
const errorMessage = ref<string>('');
|
||||
|
||||
const beforeUpload = (file: File) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleChange = (info: any) => {
|
||||
fileList.value = info.fileList;
|
||||
};
|
||||
|
||||
const calculateMD5 = (file: File): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunkSize = 2097152; // 2MB
|
||||
const chunks = Math.ceil(file.size / chunkSize);
|
||||
let currentChunk = 0;
|
||||
const spark = new SparkMD5.ArrayBuffer();
|
||||
const fileReader = new FileReader();
|
||||
|
||||
fileReader.onload = (e) => {
|
||||
if (e.target && e.target.result) {
|
||||
spark.append(e.target.result as ArrayBuffer);
|
||||
currentChunk++;
|
||||
if (currentChunk < chunks) {
|
||||
loadNext();
|
||||
} else {
|
||||
const md5 = spark.end();
|
||||
resolve(md5);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fileReader.onerror = () => {
|
||||
reject('文件读取错误');
|
||||
};
|
||||
|
||||
const loadNext = () => {
|
||||
const start = currentChunk * chunkSize;
|
||||
const end = Math.min(start + chunkSize, file.size);
|
||||
fileReader.readAsArrayBuffer(file.slice(start, end));
|
||||
};
|
||||
|
||||
loadNext();
|
||||
});
|
||||
};
|
||||
|
||||
const uploadFiles = async () => {
|
||||
if (fileList.value.length === 0) return;
|
||||
|
||||
uploadProgress.value = 0;
|
||||
uploadResult.value = null;
|
||||
errorMessage.value = '';
|
||||
|
||||
try {
|
||||
const file = fileList.value[0].raw as File;
|
||||
const fileMD5 = await calculateMD5(file);
|
||||
console.log('文件 MD5:', fileMD5);
|
||||
|
||||
// 检查文件是否已上传
|
||||
const checkResponse = await checkFileUploadedByMd5(fileMD5);
|
||||
if (checkResponse.data.exists) {
|
||||
// 文件已存在,获取文件信息
|
||||
const getFileResponse = await getUploadingFile(fileMD5);
|
||||
uploadResult.value = getFileResponse.data.file;
|
||||
ElMessage.success('文件已存在,已获取文件信息');
|
||||
emit('upload-success');
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化分片上传
|
||||
const initInfo: FileUploadInfo = {
|
||||
resourceType: 'file',
|
||||
originalName: file.name,
|
||||
extension: getFileExtension(file.name),
|
||||
size: file.size,
|
||||
savePath: `uploads/${file.name}`,
|
||||
};
|
||||
|
||||
const initResponse = await initMultiPartUpload(initInfo);
|
||||
const { uploadId, preSignedUrls, partCount } = initResponse.data.data;
|
||||
|
||||
// 分片上传
|
||||
const partSize = 5 * 1024 * 1024; // 5MB
|
||||
let uploadedBytes = 0;
|
||||
|
||||
for (let i = 0; i < partCount; i++) {
|
||||
const start = i * partSize;
|
||||
const end = Math.min(start + partSize, file.size);
|
||||
const blob = file.slice(start, end);
|
||||
const preSignedUrl = preSignedUrls[i];
|
||||
|
||||
// 正确声明配置类型为 AxiosRequestConfig<Blob>
|
||||
const config: AxiosRequestConfig = {
|
||||
headers: {
|
||||
'Content-Type': file.type, // 设置文件类型
|
||||
},
|
||||
onUploadProgress: (progressEvent: ProgressEvent) => {
|
||||
if (progressEvent.total) {
|
||||
const percentCompleted = Math.round(
|
||||
(progressEvent.loaded * 100) / progressEvent.total
|
||||
);
|
||||
uploadProgress.value = Math.min(
|
||||
percentCompleted + (i * 100) / partCount,
|
||||
100
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// 调用 PUT 请求,上传文件
|
||||
await axios.put(preSignedUrl, blob, config);
|
||||
}
|
||||
|
||||
// 完成分片上传
|
||||
const completeInfo: FileUploadInfo = {
|
||||
resourceType: 'file',
|
||||
originalName: file.name,
|
||||
extension: getFileExtension(file.name),
|
||||
size: file.size,
|
||||
savePath: `uploads/${file.name}`,
|
||||
uploadId: uploadId,
|
||||
};
|
||||
|
||||
const completeResponse = await completeMultiPartUpload(completeInfo);
|
||||
uploadResult.value = completeResponse.data.data;
|
||||
ElMessage.success('文件上传成功');
|
||||
emit('upload-success');
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
errorMessage.value = error.response?.data?.message || '上传失败';
|
||||
}
|
||||
};
|
||||
|
||||
const getFileExtension = (filename: string): string => {
|
||||
const index = filename.lastIndexOf('.');
|
||||
if (index === -1) return '';
|
||||
return filename.substring(index + 1);
|
||||
};
|
||||
|
||||
return {
|
||||
fileList,
|
||||
uploadProgress,
|
||||
uploadResult,
|
||||
errorMessage,
|
||||
beforeUpload,
|
||||
handleChange,
|
||||
uploadFiles,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.file-uploader {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
.upload-button {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.upload-progress {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.upload-result {
|
||||
margin-top: 16px;
|
||||
text-align: left;
|
||||
}
|
||||
.error-message {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
285
minio-fornt/src/components/Resource/UploadCoursewareButton.vue
Normal file
285
minio-fornt/src/components/Resource/UploadCoursewareButton.vue
Normal file
@ -0,0 +1,285 @@
|
||||
<!-- src/components/UploadCoursewareButton.vue -->
|
||||
<template>
|
||||
<div>
|
||||
<el-button type="primary" @click="showModal = true">
|
||||
上传课件
|
||||
</el-button>
|
||||
|
||||
<el-dialog
|
||||
title="上传课件"
|
||||
:visible.sync="showModal"
|
||||
width="50%"
|
||||
@close="handleCancel"
|
||||
>
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
drag
|
||||
action=""
|
||||
:before-upload="beforeUpload"
|
||||
:file-list="[]"
|
||||
multiple
|
||||
:auto-upload="false"
|
||||
:on-change="handleChange"
|
||||
>
|
||||
<i class="el-icon-upload"></i>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
<div slot="tip" class="el-upload__tip">支持多文件上传,支持word、ppt、pdf、zip、rar、txt格式文件</div>
|
||||
</el-upload>
|
||||
|
||||
<el-table
|
||||
:data="fileList"
|
||||
style="width: 100%; margin-top: 20px;"
|
||||
:show-header="false"
|
||||
row-key="id"
|
||||
>
|
||||
<el-table-column prop="name">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.file.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="size">
|
||||
<template #default="scope">
|
||||
<span>{{ (scope.row.file.size / 1024 / 1024).toFixed(2) }} MB</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="progress">
|
||||
<template #default="scope">
|
||||
<el-progress v-if="scope.row.upload.status === 'uploading'" :percentage="scope.row.upload.progress" />
|
||||
<span v-else-if="scope.row.upload.status === 'waiting'">等待上传</span>
|
||||
<span v-else-if="scope.row.upload.status === 'success'" style="color: green;">上传成功</span>
|
||||
<span v-else-if="scope.row.upload.status === 'error'" style="color: red;">{{ scope.row.upload.remark }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" @click="handleUpload">上传</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
export default {
|
||||
name: 'UploadCoursewareButton',
|
||||
props: {
|
||||
categoryIds: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['update'],
|
||||
setup(props, { emit }) {
|
||||
const showModal = ref(false);
|
||||
const fileList = ref([]);
|
||||
|
||||
// 生成唯一ID
|
||||
const generateUUID = () => {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
const r = (Math.random() * 16) | 0,
|
||||
v = c === 'x' ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
};
|
||||
|
||||
// 文件类型验证
|
||||
const allowedTypes = ['doc', 'docx', 'ppt', 'pptx', 'pdf', 'zip', 'rar', 'txt'];
|
||||
|
||||
const beforeUpload = (file) => {
|
||||
const extension = getFileExtension(file.name);
|
||||
if (!allowedTypes.includes(extension)) {
|
||||
ElMessage.error(`${file.name} 是不支持的文件类型`);
|
||||
return false; // 阻止上传
|
||||
}
|
||||
// 添加到 fileList
|
||||
const newFileItem = {
|
||||
id: generateUUID(),
|
||||
file: file,
|
||||
upload: {
|
||||
progress: 0,
|
||||
status: 'waiting', // 'waiting', 'uploading', 'success', 'error'
|
||||
remark: '',
|
||||
},
|
||||
};
|
||||
fileList.value.push(newFileItem);
|
||||
console.log('文件添加到待上传列表:', newFileItem);
|
||||
return false; // 阻止自动上传
|
||||
};
|
||||
|
||||
const handleChange = (info) => {
|
||||
// 由于使用自定义上传,这里无需处理
|
||||
console.log('文件变化:', info);
|
||||
};
|
||||
|
||||
const handleUpload = async () => {
|
||||
console.log('开始上传文件列表:', fileList.value);
|
||||
for (const fileItem of fileList.value) {
|
||||
if (fileItem.upload.status === 'waiting') {
|
||||
await uploadFile(fileItem);
|
||||
}
|
||||
}
|
||||
emit('update');
|
||||
};
|
||||
|
||||
const uploadFile = async (fileItem) => {
|
||||
fileItem.upload.status = 'uploading';
|
||||
console.log(`开始上传文件: ${fileItem.file.name}`);
|
||||
try {
|
||||
// 获取上传ID
|
||||
const uploadIdResponse = await minioUploadId(getFileExtension(fileItem.file.name));
|
||||
const { resource_type, upload_id, filename } = uploadIdResponse.data;
|
||||
console.log('获取上传ID成功:', uploadIdResponse.data);
|
||||
|
||||
const chunkSize = 5 * 1024 * 1024; // 5MB
|
||||
const totalChunks = Math.ceil(fileItem.file.size / chunkSize);
|
||||
let currentChunk = 0;
|
||||
|
||||
while (currentChunk < totalChunks) {
|
||||
const start = currentChunk * chunkSize;
|
||||
const end = Math.min(start + chunkSize, fileItem.file.size);
|
||||
const blob = fileItem.file.slice(start, end);
|
||||
|
||||
// 获取预签名URL
|
||||
const preSignUrlResponse = await minioPreSignUrl(upload_id, filename, currentChunk + 1);
|
||||
const { url } = preSignUrlResponse.data;
|
||||
console.log(`获取预签名URL成功 (分片 ${currentChunk + 1}):`, url);
|
||||
|
||||
// 上传分片
|
||||
const uploadResponse = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': fileItem.file.type,
|
||||
},
|
||||
body: blob,
|
||||
});
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
throw new Error(`分片 ${currentChunk + 1} 上传失败`);
|
||||
}
|
||||
|
||||
currentChunk++;
|
||||
fileItem.upload.progress = Math.floor((currentChunk / totalChunks) * 100);
|
||||
console.log(`分片 ${currentChunk} 上传成功,进度: ${fileItem.upload.progress}%`);
|
||||
// 触发响应式更新
|
||||
fileList.value = [...fileList.value];
|
||||
}
|
||||
|
||||
// 合并分片
|
||||
const mergeData = {
|
||||
extension: getFileExtension(fileItem.file.name),
|
||||
original_filename: fileItem.file.name,
|
||||
category_ids: props.categoryIds.join(','),
|
||||
size: fileItem.file.size,
|
||||
upload_id: upload_id,
|
||||
filename: filename,
|
||||
poster: '',
|
||||
};
|
||||
|
||||
const mergeResponse = await minioMergeFile(mergeData);
|
||||
const { url: mergedUrl } = mergeResponse.data;
|
||||
console.log('合并分片成功:', mergeResponse.data);
|
||||
|
||||
fileItem.upload.progress = 100;
|
||||
fileItem.upload.status = 'success';
|
||||
ElMessage.success(`${fileItem.file.name} 上传成功`);
|
||||
} catch (error) {
|
||||
console.error(`上传文件失败 (${fileItem.file.name}):`, error);
|
||||
fileItem.upload.status = 'error';
|
||||
fileItem.upload.remark = error.message || '上传失败';
|
||||
ElMessage.error(`${fileItem.file.name} 上传失败: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
// 取消上传中或等待上传的文件
|
||||
fileList.value.forEach((item) => {
|
||||
if (item.upload.status !== 'success') {
|
||||
item.upload.status = 'error';
|
||||
item.upload.remark = '上传被取消';
|
||||
console.log(`上传被取消: ${item.file.name}`);
|
||||
}
|
||||
});
|
||||
fileList.value = [];
|
||||
showModal.value = false;
|
||||
emit('update');
|
||||
};
|
||||
|
||||
const getFileExtension = (filename) => {
|
||||
const index = filename.lastIndexOf('.');
|
||||
if (index === -1) return '';
|
||||
return filename.substring(index + 1).toLowerCase();
|
||||
};
|
||||
|
||||
// 模拟API调用
|
||||
const minioUploadId = async (extension) => {
|
||||
// 替换为实际的API调用
|
||||
console.log('请求获取上传ID');
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
data: {
|
||||
resource_type: 'courseware',
|
||||
upload_id: 'unique-upload-id',
|
||||
filename: 'uploaded-file-name.' + extension,
|
||||
},
|
||||
});
|
||||
}, 500);
|
||||
});
|
||||
};
|
||||
|
||||
const minioPreSignUrl = async (upload_id, filename, chunkNumber) => {
|
||||
// 替换为实际的API调用
|
||||
console.log(`请求获取预签名URL (分片 ${chunkNumber})`);
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
data: {
|
||||
url: `https://example.com/upload/${upload_id}/${filename}/chunk-${chunkNumber}`,
|
||||
},
|
||||
});
|
||||
}, 500);
|
||||
});
|
||||
};
|
||||
|
||||
const minioMergeFile = async (mergeData) => {
|
||||
// 替换为实际的API调用
|
||||
console.log('请求合并分片');
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
data: {
|
||||
url: 'https://example.com/upload/complete',
|
||||
},
|
||||
});
|
||||
}, 500);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
showModal,
|
||||
fileList,
|
||||
beforeUpload,
|
||||
handleChange,
|
||||
handleUpload,
|
||||
handleCancel,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.upload-demo i {
|
||||
font-size: 28px;
|
||||
color: #409EFF;
|
||||
}
|
||||
.upload-demo .el-upload__text {
|
||||
margin-top: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.upload-demo .el-upload__tip {
|
||||
font-size: 12px;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
</style>
|
@ -1,13 +1,19 @@
|
||||
import {createApp} from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
// src/main.js
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
|
||||
const app = createApp(App)
|
||||
// 引入 Element Plus 及其样式
|
||||
import ElementPlus from 'element-plus';
|
||||
import 'element-plus/dist/index.css';
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(router)
|
||||
app.use(ElementPlus)
|
||||
// 使用 Element Plus
|
||||
app.use(ElementPlus);
|
||||
|
||||
app.mount('#app')
|
||||
// 使用路由
|
||||
app.use(router);
|
||||
|
||||
// 挂载应用
|
||||
app.mount('#app');
|
||||
|
11
minio-fornt/src/utils/dateFormat.js
Normal file
11
minio-fornt/src/utils/dateFormat.js
Normal file
@ -0,0 +1,11 @@
|
||||
// src/utils/dateFormat.js
|
||||
export const dateFormat = (dateStr) => {
|
||||
const date = new Date(dateStr);
|
||||
const year = date.getFullYear();
|
||||
const month = (`0${date.getMonth() + 1}`).slice(-2);
|
||||
const day = (`0${date.getDate()}`).slice(-2);
|
||||
const hours = (`0${date.getHours()}`).slice(-2);
|
||||
const minutes = (`0${date.getMinutes()}`).slice(-2);
|
||||
const seconds = (`0${date.getSeconds()}`).slice(-2);
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
@ -1,42 +1,34 @@
|
||||
import axios from 'axios'
|
||||
// src/utils/request.js
|
||||
import axios from 'axios';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const request = axios.create({
|
||||
baseURL: `http://localhost:9090`,
|
||||
timeout: 30000
|
||||
})
|
||||
|
||||
// request 拦截器
|
||||
// 可以自请求发送前对请求做一些处理
|
||||
// 比如统一加token,对请求参数统一加密
|
||||
request.interceptors.request.use(config => {
|
||||
config.headers['Content-Type'] = 'application/json;charset=utf-8';
|
||||
return config
|
||||
}, error => {
|
||||
return Promise.reject(error)
|
||||
// 创建 axios 实例
|
||||
const instance = axios.create({
|
||||
baseURL: 'http://localhost:9090', // 替换为您的后端地址
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// response 拦截器
|
||||
// 可以在接口响应后统一处理结果
|
||||
request.interceptors.response.use(
|
||||
response => {
|
||||
let res = response.data;
|
||||
// 如果是返回的文件
|
||||
if (response.headers === 'blob') {
|
||||
return res
|
||||
}
|
||||
// 兼容服务端返回的字符串数据
|
||||
if (typeof res === 'string') {
|
||||
res = res ? JSON.parse(res) : res
|
||||
console.log(res)
|
||||
}
|
||||
return res;
|
||||
// 请求拦截器
|
||||
instance.interceptors.request.use(
|
||||
(config) => {
|
||||
// 可以在这里添加请求头,例如 token
|
||||
// config.headers['Authorization'] = 'Bearer token';
|
||||
return config;
|
||||
},
|
||||
error => {
|
||||
console.log('err' + error) // for debug
|
||||
return Promise.reject(error)
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// 响应拦截器
|
||||
instance.interceptors.response.use(
|
||||
(response) => {
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
ElMessage.error(error.response?.data?.message || '请求失败');
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default request
|
||||
|
||||
export default instance;
|
||||
|
@ -1,338 +1,59 @@
|
||||
<!-- src/views/FileView.vue -->
|
||||
<template>
|
||||
<div class="container">
|
||||
<div style="display:none;">
|
||||
<video width="500" height="240" controls id="upvideo">
|
||||
</video>
|
||||
</div>
|
||||
<h2>上传示例</h2>
|
||||
<div class="upload-demo">
|
||||
<el-upload ref="upload" action="https://jsonplaceholder.typicode.com/posts/"
|
||||
:on-remove="handleRemove" :on-change="handleFileChange" :file-list="data.uploadFileList"
|
||||
:show-file-list="false"
|
||||
:auto-upload="false" multiple>
|
||||
<el-button slot="trigger" type="primary" plain>选择文件</el-button>
|
||||
</el-upload>
|
||||
<el-button style="margin: 5px;" type="success" @click="handler">上传</el-button>
|
||||
<el-button type="danger" @click="clearFileHandler">清空</el-button>
|
||||
</div>
|
||||
<table style="margin-top: 20px">
|
||||
<th>
|
||||
文件名
|
||||
</th>
|
||||
<th>
|
||||
文件大小
|
||||
</th>
|
||||
<th>
|
||||
上传进度
|
||||
</th>
|
||||
<th>
|
||||
状态
|
||||
</th>
|
||||
</table>
|
||||
<!-- 文件列表 -->
|
||||
<div class="file-list-wrapper">
|
||||
<el-collapse>
|
||||
<el-collapse-item v-for="item in data.uploadFileList">
|
||||
<template #title>
|
||||
<div class="upload-file-item">
|
||||
<div class="file-info-item file-name" :title="item.name">{{ item.name }}</div>
|
||||
<div class="file-info-item file-size">{{ item.size }}</div>
|
||||
<div class="file-info-item file-progress">
|
||||
<span class="file-progress-label"></span>
|
||||
<el-progress :percentage="item.uploadProgress" class="file-progress-value"/>
|
||||
</div>
|
||||
<div class="file-info-item file-size"><span></span>
|
||||
<el-tag v-if="item.status === '等待上传'" size="small" type="info">等待上传</el-tag>
|
||||
<el-tag v-else-if="item.status === '校验MD5'" size="small" type="warning">校验MD5</el-tag>
|
||||
<el-tag v-else-if="item.status === '正在上传'" size="small">正在上传</el-tag>
|
||||
<el-tag v-else-if="item.status === '上传成功'" size="small" type="success">上传完成</el-tag>
|
||||
<el-tag v-else size="small">正在上传</el-tag>
|
||||
<!-- <el-tag v-else size="medium" type="danger">上传错误</el-tag>-->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="file-chunk-list-wrapper">
|
||||
<!-- 分片列表 -->
|
||||
<el-table :data="item.chunkList" max-height="400" style="width: 100%">
|
||||
<el-table-column prop="chunkNumber" label="分片序号" width="180">
|
||||
</el-table-column>
|
||||
<el-table-column prop="progress" label="上传进度">
|
||||
<template v-slot="{ row }">
|
||||
<el-progress v-if="!row.status || row.progressStatus === 'normal'"
|
||||
:percentage="row.progress"/>
|
||||
<el-progress v-else :percentage="row.progress" :status="row.progressStatus"
|
||||
:text-inside="true" :stroke-width="16"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="180">
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
<div class="file-view">
|
||||
<h2>文件上传</h2>
|
||||
<FileUploader @upload-success="handleUploadSuccess" />
|
||||
<el-button type="primary" class="mt-16" @click="createBucket">
|
||||
创建 Bucket
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, reactive} from 'vue';
|
||||
import {checkUpload, initUpload, mergeUpload, uploadFileInfo} from '@/api/upload';
|
||||
import {fileSuffixTypeUtil} from '@/utils/FileUtil';
|
||||
import axios from 'axios';
|
||||
import SparkMD5 from 'spark-md5';
|
||||
import {ElMessageBox} from "element-plus";
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import FileUploader from '../components/Resource/FileUploader.vue';
|
||||
import { createBucket } from '../api/upload.js';
|
||||
import { ElInput, ElButton, ElMessage } from 'element-plus';
|
||||
|
||||
const FILE_UPLOAD_ID_KEY = 'file_upload_id';
|
||||
const chunkSize = 10 * 1024 * 1024; // 10MB
|
||||
let currentFileIndex = 0;
|
||||
export default defineComponent({
|
||||
name: 'FileView',
|
||||
components: {
|
||||
FileUploader,
|
||||
},
|
||||
setup() {
|
||||
const handleUploadSuccess = () => {
|
||||
ElMessage.success('文件上传成功!');
|
||||
// 可以在这里执行其他操作,如刷新列表等
|
||||
};
|
||||
|
||||
const FileStatus = {
|
||||
wait: '等待上传',
|
||||
getMd5: '校验MD5',
|
||||
chip: '正在创建序列',
|
||||
uploading: '正在上传',
|
||||
success: '上传成功',
|
||||
error: '上传错误'
|
||||
};
|
||||
const createBucket = async () => {
|
||||
const bucketName = prompt('请输入要创建的 Bucket 名称');
|
||||
if (!bucketName) {
|
||||
ElMessage.warning('Bucket 名称不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
const simultaneousUploads = ref(3);
|
||||
const data = reactive({
|
||||
uploadFileList: []
|
||||
try {
|
||||
await createBucket();
|
||||
ElMessage.success('Bucket 创建成功');
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.response?.data?.message || 'Bucket 创建失败');
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
handleUploadSuccess,
|
||||
createBucket,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const handler = () => {
|
||||
if (data.uploadFileList.length === 0) {
|
||||
ElMessageBox.alert('请先选择文件')
|
||||
return false;
|
||||
}
|
||||
if (currentFileIndex >= data.uploadFileList.length) {
|
||||
ElMessageBox.alert('文件上传完成')
|
||||
return false;
|
||||
}
|
||||
const currentFile = data.uploadFileList[currentFileIndex];
|
||||
currentFile.status = FileStatus.getMd5;
|
||||
currentFile.chunkUploadedList = [];
|
||||
|
||||
getFileMd5(currentFile.raw, async (md5, totalChunks) => {
|
||||
const checkResult = await checkFileUploadedByMd5(md5);
|
||||
if (checkResult.code === 1) {
|
||||
currentFile.status = FileStatus.success;
|
||||
currentFile.uploadProgress = 100;
|
||||
currentFileIndex++;
|
||||
handler();
|
||||
return;
|
||||
} else if (checkResult.code === 2) {
|
||||
currentFile.status = FileStatus.uploading;
|
||||
currentFile.chunkUploadedList = checkResult.data.chunkUploadedList;
|
||||
} else {
|
||||
console.log('未上传');
|
||||
}
|
||||
|
||||
currentFile.status = FileStatus.chip;
|
||||
let fileChunks = createFileChunk(currentFile.raw, chunkSize);
|
||||
let type = fileSuffixTypeUtil(currentFile.name);
|
||||
let param = {
|
||||
fileName: currentFile.name,
|
||||
fileSize: currentFile.size,
|
||||
chunkSize: chunkSize,
|
||||
chunkNum: totalChunks,
|
||||
fileMd5: md5,
|
||||
contentType: 'application/octet-stream',
|
||||
fileType: type,
|
||||
chunkUploadedList: currentFile.chunkUploadedList
|
||||
};
|
||||
|
||||
let uploadIdInfoResult = await getFileUploadUrls(param);
|
||||
let uploadIdInfo = uploadIdInfoResult.data;
|
||||
let uploadUrls = uploadIdInfo.urlList;
|
||||
|
||||
currentFile.chunkList = [];
|
||||
if (uploadUrls && fileChunks.length !== uploadUrls.length) {
|
||||
await ElMessageBox.alert('文件上传完成')
|
||||
return;
|
||||
}
|
||||
|
||||
fileChunks.map((chunkItem, index) => {
|
||||
if (currentFile.chunkUploadedList.indexOf(index + 1) !== -1) {
|
||||
currentFile.chunkList.push({
|
||||
chunkNumber: index + 1,
|
||||
chunk: chunkItem,
|
||||
uploadUrl: uploadUrls[index],
|
||||
progress: 100,
|
||||
progressStatus: 'success',
|
||||
status: '上传成功'
|
||||
});
|
||||
} else {
|
||||
currentFile.chunkList.push({
|
||||
chunkNumber: index + 1,
|
||||
chunk: chunkItem,
|
||||
uploadUrl: uploadUrls[index],
|
||||
progress: 0,
|
||||
status: '—'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let tempFileChunks = [];
|
||||
currentFile.chunkList.forEach((item) => {
|
||||
tempFileChunks.push(item);
|
||||
});
|
||||
|
||||
currentFile.status = FileStatus.uploading;
|
||||
tempFileChunks = processUploadChunkList(tempFileChunks);
|
||||
await uploadChunkBase(tempFileChunks);
|
||||
|
||||
if (uploadIdInfo.uploadId === "SingleFileUpload") {
|
||||
currentFile.status = FileStatus.success;
|
||||
currentFileIndex++;
|
||||
handler();
|
||||
return;
|
||||
} else {
|
||||
const mergeResult = await mergeFile({
|
||||
uploadId: uploadIdInfo.uploadId,
|
||||
fileName: currentFile.name,
|
||||
fileMd5: md5,
|
||||
fileType: type,
|
||||
chunkNum: uploadIdInfo.urlList.length,
|
||||
chunkSize: chunkSize,
|
||||
fileSize: currentFile.size
|
||||
});
|
||||
|
||||
if (!mergeResult.data) {
|
||||
currentFile.status = FileStatus.error;
|
||||
this.$message.error(mergeResult.error);
|
||||
} else {
|
||||
localStorage.removeItem(FILE_UPLOAD_ID_KEY);
|
||||
currentFile.status = FileStatus.success;
|
||||
currentFileIndex++;
|
||||
handler();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const clearFileHandler = () => {
|
||||
data.uploadFileList.splice(0, data.uploadFileList.length);
|
||||
currentFileIndex = 0;
|
||||
};
|
||||
|
||||
const handleFileChange = (file, fileList) => {
|
||||
initFileProperties(file);
|
||||
data.uploadFileList.splice(0, data.uploadFileList.length, ...fileList);
|
||||
console.log("data.uploadFileList", data.uploadFileList)
|
||||
};
|
||||
|
||||
const initFileProperties = (file) => {
|
||||
file.chunkList = [];
|
||||
file.status = FileStatus.wait;
|
||||
file.progressStatus = 'warning';
|
||||
file.uploadProgress = 0;
|
||||
};
|
||||
|
||||
const handleRemove = (file, fileList) => {
|
||||
data.uploadFileList.splice(0, data.uploadFileList.length, ...fileList);
|
||||
};
|
||||
|
||||
const getFileMd5 = (file, callback) => {
|
||||
let fileReader = new FileReader();
|
||||
fileReader.readAsArrayBuffer(file);
|
||||
fileReader.onload = function (e) {
|
||||
let spark = new SparkMD5.ArrayBuffer();
|
||||
spark.append(e.target.result);
|
||||
callback(spark.end(), Math.ceil(file.size / chunkSize));
|
||||
};
|
||||
};
|
||||
|
||||
const createFileChunk = (file, size) => {
|
||||
let fileChunks = [];
|
||||
let cur = 0;
|
||||
while (cur < file.size) {
|
||||
fileChunks.push({file: file.slice(cur, cur + size)});
|
||||
cur += size;
|
||||
}
|
||||
return fileChunks;
|
||||
};
|
||||
|
||||
const checkFileUploadedByMd5 = async (md5) => {
|
||||
const response = await checkUpload(md5);
|
||||
return response;
|
||||
};
|
||||
|
||||
const getFileUploadUrls = async (param) => {
|
||||
const response = await initUpload(param);
|
||||
return response;
|
||||
};
|
||||
|
||||
const processUploadChunkList = (chunkList) => {
|
||||
const temp = [];
|
||||
chunkList.forEach((chunk) => {
|
||||
temp.push(chunk);
|
||||
});
|
||||
return temp;
|
||||
};
|
||||
|
||||
const uploadChunkBase = async (chunkList) => {
|
||||
const uploadPromiseList = [];
|
||||
const limit = simultaneousUploads.value;
|
||||
for (let i = 0; i < chunkList.length; i++) {
|
||||
if (chunkList[i].progress !== 100) {
|
||||
chunkList[i].status = FileStatus.uploading;
|
||||
let params = chunkList[i];
|
||||
uploadPromiseList.push(
|
||||
uploadFileChunk(params)
|
||||
.then(() => {
|
||||
chunkList[i].progress = 100;
|
||||
chunkList[i].progressStatus = 'success';
|
||||
chunkList[i].status = '上传成功';
|
||||
})
|
||||
.catch(() => {
|
||||
chunkList[i].progress = 100;
|
||||
chunkList[i].progressStatus = 'exception';
|
||||
chunkList[i].status = '上传失败';
|
||||
})
|
||||
);
|
||||
}
|
||||
if (uploadPromiseList.length === limit || i === chunkList.length - 1) {
|
||||
await Promise.all(uploadPromiseList);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const uploadFileChunk = async (chunk) => {
|
||||
let formData = new FormData();
|
||||
formData.append('file', chunk.chunk.file);
|
||||
await axios.put(chunk.uploadUrl, formData, {
|
||||
headers: {'Content-Type': 'application/octet-stream'}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
.file-view {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.file-list-wrapper {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.upload-file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.file-info-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
text-align: left;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.file-progress {
|
||||
width: 200px;
|
||||
margin: 0 20px;
|
||||
.mt-16 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
|
Loading…
Reference in New Issue
Block a user