前端逻辑更新

This commit is contained in:
黄子寒 2024-12-16 10:58:49 +08:00
parent a1e5554823
commit 9b1a2f51e8
11 changed files with 767 additions and 405 deletions

View 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);
}
}

View File

@ -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>

View 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),
};

View File

@ -0,0 +1,6 @@
// src/api/resourceCategory.js
import axios from '../utils/request';
export const resourceCategory = {
resourceCategoryList: () => axios.get('/resource-category/list'),
};

View File

@ -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 },
});
};

View 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>

View 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">支持多文件上传支持wordpptpdfziprartxt格式文件</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>

View File

@ -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');

View 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}`;
};

View File

@ -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;

View File

@ -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>