前端:
安装依赖
1.在package.json文件中添加"vue-simple-uploader": "^0.7.4","spark-md5": "^3.0.1"到dependencies中;devDependencies中"node-sass": "^4.9.0",
"sass-loader": "^7.1.0",
npm install jquery --save;
2.并重新npm install
使用依赖
在main.js中使用
//引入
import uploader from 'vue-simple-uploader'
// 全局组件挂载
Vue.use(uploader)
添加页面代码
工具类组件 fileuploader
<template>
<div id="global-uploader">
<!-- 上传 -->
<uploader
ref="uploader"
:options="options"
:autoStart="false"
@file-added="onFileAdded"
@file-success="onFileSuccess"
@file-progress="onFileProgress"
@file-error="onFileError"
class="uploader-app">
<uploader-unsupport></uploader-unsupport>
<uploader-btn id="global-uploader-btn" :attrs="attrs" ref="uploadBtn">选择文件</uploader-btn>
<uploader-list v-show="panelShow">
<div class="file-panel" slot-scope="props" :class="{'collapse': collapse}">
<div class="file-title">
<h2>文件列表</h2>
<div class="operate">
<el-button @click="fileListShow" type="text" :title="collapse ? '展开':'折叠' ">
<i class="el-icon-d-caret" style="color:black;font-size: 18px"
:class="collapse ? 'inuc-fullscreen': 'inuc-minus-round'"></i>
</el-button>
<el-button @click="close" type="text" title="关闭">
<i class="el-icon-close" style="color:black;font-size: 18px"></i>
</el-button>
</div>
</div>
<ul class="file-list">
<li v-for="file in props.fileList" :key="file.id">
<uploader-file :class="'file_' + file.id" ref="files" :file="file" :list="true"></uploader-file>
</li>
<div class="no-file" v-if="!props.fileList.length"><i class="iconfont icon-empty-file"></i> 暂无待上传文件</div>
</ul>
</div>
</uploader-list>
</uploader>
</div>
</template>
<script>
/**
* 全局上传插件
* 调用方法:Bus.$emit('openUploader', {}) 打开文件选择框,参数为需要传递的额外参数
* 监听函数:Bus.$on('fileAdded', fn); 文件选择后的回调
* Bus.$on('fileSuccess', fn); 文件上传成功的回调
*/
import {ACCEPT_CONFIG} from '../../../assets/js/config';
import Bus from '../../../assets/js/bus';
import SparkMD5 from 'spark-md5';
import {fileMerge} from '@/api/background/fileuploader/fileuploader';
import $ from 'jquery';
export default {
data() {
return {
options: {
target: process.env.VUE_APP_BASE_API+'/bigfileupload/background/file/upload',
chunkSize: 5 * 1024 * 1000,
fileParameterName: 'file',
maxChunkRetries: 2,
testChunks: true, //是否开启服务器分片校验
checkChunkUploadedByResponse: function (chunk, message) {
// 服务器分片校验函数,秒传及断点续传基础
let objMessage = JSON.parse(message);
if (objMessage.skipUpload) {
return true;
}
return (objMessage.uploaded || []).indexOf(chunk.offset + 1) >= 0
},
headers: {
Authorization: ''
},
query() {
}
},
attrs: {
accept: ACCEPT_CONFIG.getAll()
},
panelShow: false, //选择文件后,展示上传panel
collapse: false
}
},
mounted() {
Bus.$on('openUploader', query => {
this.params = query || {};
this.options.headers.Authorization = 'Bearer ' + query.token
if (this.$refs.uploadBtn) {
$("#global-uploader-btn").click();
}
});
},
computed: {
//Uploader实例
uploader() {
return this.$refs.uploader.uploader;
}
},
methods: {
onFileAdded(file) {
this.panelShow = true;
this.computeMD5(file);
alert(JSON.stringify(file))
Bus.$emit('fileAdded');
},
onFileProgress(rootFile, file, chunk) {
console.log(`上传中 ${file.name},chunk:${chunk.startByte / 1024 / 1024} ~ ${chunk.endByte / 1024 / 1024}`)
},
onFileSuccess(rootFile, file, response, chunk) {
let res = JSON.parse(response);
// TODO 如有需要 解开注释 和后台协议如何处理这种情况:服务器自定义的错误(即虽返回200,但是是错误的情况),这种错误是Uploader无法拦截的
// if (!res.result) {
// this.$message({message: res.message, type: 'error'});
// // 文件状态设为“失败”
// this.statusSet(file.id, 'failed');
// return
// }
// 如果服务端返回需要合并
if (res.needMerge) {
// 文件状态设为“合并中”
this.statusSet(file.id, 'merging');
let param = {
'filename': rootFile.name,
'identifier': rootFile.uniqueIdentifier,
'totalSize': rootFile.size
}
fileMerge(param).then(res => {
console.log(res);
// 文件合并成功
Bus.$emit('fileSuccess',res);
this.statusRemove(file.id);
}).catch(e => {
console.log("合并异常,重新发起请求,文件名为:", file.name)
//由于网络或服务器原因,导致合并过程中断线,此时如果不重新发起请求,就会进入失败的状态,导致该文件无法重试
file.retry();
});
// 不需要合并
} else {
Bus.$emit('fileSuccess');
alert(JSON.stringify(file))
console.log('上传成功');
}
},
onFileError(rootFile, file, response, chunk) {
this.$message({
message: response,
type: 'error'
})
},
/**
* 计算md5,实现断点续传及秒传
* @param file
*/
computeMD5(file) {
let fileReader = new FileReader();
let time = new Date().getTime();
let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
let currentChunk = 0;
const chunkSize = 10 * 1024 * 1000;
let chunks = Math.ceil(file.size / chunkSize);
let spark = new SparkMD5.ArrayBuffer();
// 文件状态设为"计算MD5"
this.statusSet(file.id, 'md5');
file.pause();
loadNext();
fileReader.onload = (e => {
spark.append(e.target.result);
if (currentChunk < chunks) {
currentChunk++;
loadNext();
// 实时展示MD5的计算进度
this.$nextTick(() => {
$(`.myStatus_${file.id}`).text('校验MD5 ' + ((currentChunk / chunks) * 100).toFixed(0) + '%')
})
} else {
let md5 = spark.end();
this.computeMD5Success(md5, file);
console.log(`MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${file.size} 用时:${new Date().getTime() - time} ms`);
}
});
fileReader.onerror = function () {
this.error(`文件${file.name}读取出错,请检查该文件`)
file.cancel();
};
function loadNext() {
let start = currentChunk * chunkSize;
let end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));
}
},
computeMD5Success(md5, file) {
// 将自定义参数直接加载uploader实例的opts上
Object.assign(this.uploader.opts, {
query: {
...this.params,
}
})
file.uniqueIdentifier = md5;
file.resume();
this.statusRemove(file.id);
},
fileListShow() {
let $list = $('#global-uploader .file-list');
if ($list.is(':visible')) {
$list.slideUp();
this.collapse = true;
} else {
$list.slideDown();
this.collapse = false;
}
},
close() {
this.uploader.cancel();
this.panelShow = false;
},
/**
* 新增的自定义的状态: 'md5'、'transcoding'、'failed'
* @param id
* @param status
*/
statusSet(id, status) {
let statusMap = {
md5: {
text: '校验MD5',
bgc: '#fff'
},
merging: {
text: '合并中',
bgc: '#e2eeff'
},
transcoding: {
text: '转码中',
bgc: '#e2eeff'
},
failed: {
text: '上传失败',
bgc: '#e2eeff'
}
}
this.$nextTick(() => {
$(`<p class="myStatus_${id}"></p>`).appendTo(`.file_${id} .uploader-file-status`).css({
'position': 'absolute',
'top': '0',
'left': '0',
'right': '0',
'bottom': '0',
'zIndex': '1',
'line-height': 'initial',
'backgroundColor': statusMap[status].bgc
}).text(statusMap[status].text);
})
},
statusRemove(id) {
this.$nextTick(() => {
$(`.myStatus_${id}`).remove();
})
},
error(msg) {
this.$notify({
title: '错误',
message: msg,
type: 'error',
duration: 2000
})
}
},
watch: {},
destroyed() {
Bus.$off('openUploader');
},
components: {}
}
</script>
<style scoped lang="scss">
#global-uploader {
position: fixed;
z-index: 20;
right: 15px;
bottom: 15px;
h1, h2 {
font-weight: normal;
}
ul {
list-style-type: none;
padding: 0px 4px;
}
li {
display: inline-block;
}
.uploader-app {
width: 660px;
}
.file-panel {
background-color: #fff;
border: 1px solid #e2e2e2;
border-radius: 7px 7px 0 0;
box-shadow: 0 0 10px rgba(0, 0, 0, .2);
.file-title {
display: flex;
height: 40px;
line-height: 0px;
padding: 0 15px;
border-bottom: 1px solid #ddd;
.operate {
flex: 1;
text-align: right;
}
}
.file-list {
position: relative;
height: 264px;
width: 654px;
overflow-x: hidden;
overflow-y: auto;
background-color: #fff;
> li {
background-color: #fff;
}
}
&.collapse {
.file-title {
background-color: #E7ECF2;
}
}
}
.no-file {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 16px;
}
.uploader-file {
width: 654px;
}
/deep/ .uploader-file-icon {
&:before {
content: '' !important;
}
&[icon=image] {
background: url(../../../assets/image/image-icon.png);
}
&[icon=video] {
background: url(../../../assets/image/video-icon.png);
}
&[icon=document] {
background: url(../../../assets/image/text-icon.png);
}
}
/deep/ .uploader-file-actions > span {
margin-right: 6px;
}
}
/* 隐藏上传按钮 */
#global-uploader-btn {
position: absolute;
clip: rect(0, 0, 0, 0);
}
/*.uploader-list>ul>li{*/
/* width:100%;*/
/* color:red;*/
/* margin-bottom: 0;*/
/*}*/
/*a {*/
/* color: #42b983;*/
/*}*/
</style>
展现效果的组件 filelist
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" :inline="true" label-width="68px">
<el-form-item label="文件名" prop="filename">
<el-input
v-model="queryParams.filename"
placeholder="请输入文件名"
clearable
size="small"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item>
<div>
</div>
<el-button type="primary" icon="el-icon-upload" size="mini" @click="upload">上传</el-button>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="warning"
icon="el-icon-download"
size="mini"
@click="handleExport"
v-hasPermi="['background:filelist:export']"
>导出</el-button>
</el-col>
</el-row>
<el-table v-loading="loading" :data="filelistList" @selection-change="handleSelectionChange">
<el-table-column label="文件名" align="center" prop="filename" />
<el-table-column label="本地地址" align="center" prop="location" />
<el-table-column label="文件总大小" align="center" prop="totalSize" />
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改已上传文件列表对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px">
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="文件名" prop="filename">
<el-input v-model="form.filename" placeholder="请输入文件名" />
</el-form-item>
<el-form-item label="唯一标识,MD5" prop="identifier">
<el-input v-model="form.identifier" placeholder="请输入唯一标识,MD5" />
</el-form-item>
<el-form-item label="链接" prop="url">
<el-input v-model="form.url" placeholder="请输入链接" />
</el-form-item>
<el-form-item label="本地地址" prop="location">
<el-input v-model="form.location" placeholder="请输入本地地址" />
</el-form-item>
<el-form-item label="文件总大小" prop="totalSize">
<el-input v-model="form.totalSize" placeholder="请输入文件总大小" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listFilelist, getFilelist, delFilelist, addFilelist, updateFilelist, exportFilelist } from "@/api/background/filelist";
import Bus from '../../../assets/js/bus';
import {getToken} from '@/utils/auth'
export default {
name: "Filelist",
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 总条数
total: 0,
// 已上传文件列表表格数据
filelistList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
filename: undefined,
identifier: undefined,
url: undefined,
location: undefined,
totalSize: undefined
},
// 表单参数
form: {},
// 表单校验
rules: {
filename: [
{ required: true, message: "文件名不能为空", trigger: "blur" }
],
identifier: [
{ required: true, message: "唯一标识,MD5不能为空", trigger: "blur" }
],
url: [
{ required: true, message: "链接不能为空", trigger: "blur" }
],
}
};
},
created() {
this.getList();
},
methods: {
upload() {
// 打开文件选择框
Bus.$emit('openUploader', {
token:getToken()
})
},
/** 查询已上传文件列表列表 */
getList() {
this.loading = true;
listFilelist(this.queryParams).then(response => {
this.filelistList = response.rows;
this.total = response.total;
this.loading = false;
});
},
// 取消按钮
cancel() {
this.open = false;
this.reset();
},
// 表单重置
reset() {
this.form = {
id: undefined,
filename: undefined,
identifier: undefined,
url: undefined,
location: undefined,
totalSize: undefined
};
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.id)
this.single = selection.length!=1
this.multiple = !selection.length
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.open = true;
this.title = "添加已上传文件列表";
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset();
const id = row.id || this.ids
getFilelist(id).then(response => {
this.form = response.data;
this.open = true;
this.title = "修改已上传文件列表";
});
},
/** 提交按钮 */
submitForm: function() {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.id != undefined) {
updateFilelist(this.form).then(response => {
if (response.code === 200) {
this.msgSuccess("修改成功");
this.open = false;
this.getList();
} else {
this.msgError(response.msg);
}
});
} else {
addFilelist(this.form).then(response => {
if (response.code === 200) {
this.msgSuccess("新增成功");
this.open = false;
this.getList();
} else {
this.msgError(response.msg);
}
});
}
}
});
},
/** 删除按钮操作 */
handleDelete(row) {
const ids = row.id || this.ids;
this.$confirm('是否确认删除已上传文件列表编号为"' + ids + '"的数据项?', "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(function() {
return delFilelist(ids);
}).then(() => {
this.getList();
this.msgSuccess("删除成功");
}).catch(function() {});
},
/** 导出按钮操作 */
handleExport() {
const queryParams = this.queryParams;
this.$confirm('是否确认导出所有已上传文件列表数据项?', "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(function() {
return exportFilelist(queryParams);
}).then(response => {
this.download(response.msg);
}).catch(function() {});
}
},
mounted() {
// 文件选择后的回调
Bus.$on('fileAdded', () => {
console.log('文件已选择')
});
// 文件上传成功的回调
Bus.$on('fileSuccess', (res ) => {
alert("文件上传成功"+JSON.stringify(res.data))
});
}
};
</script>
添加工具
bus.js
import Vue from 'vue';
export default new Vue();
config.js
export const ACCEPT_CONFIG = {
image: ['.png', '.jpg', '.jpeg', '.gif', '.bmp'],
video: ['.mp4', '.rmvb', '.mkv', '.wmv', '.flv'],
document: ['.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.pdf', '.txt', '.tif', '.tiff','.zip','.7z','.rar'],
getAll(){
return [...this.image, ...this.video, ...this.document]
},
};
添加接口文件
fileuploader
import request from '@/utils/request'
// 查询公告列表
export function fileMerge(param) {
return request({
url: '/你的服务名/background/file/merge',
method: 'post',
params: param
})
}
filelist
import request from '@/utils/request'
// 查询已上传文件列表列表
export function listFilelist(query) {
return request({
url: '/你的服务名/background/filelist/list',
method: 'get',
params: query
})
}
// 查询已上传文件列表详细
export function getFilelist(id) {
return request({
url: '/你的服务名/background/filelist/' + id,
method: 'get'
})
}
// 新增已上传文件列表
export function addFilelist(data) {
return request({
url: '/你的服务名/background/filelist',
method: 'post',
data: data
})
}
// 修改已上传文件列表
export function updateFilelist(data) {
return request({
url: '/你的服务名/background/filelist',
method: 'put',
data: data
})
}
// 删除已上传文件列表
export function delFilelist(id) {
return request({
url: '/你的服务名/background/filelist/' + id,
method: 'delete'
})
}
// 导出已上传文件列表
export function exportFilelist(query) {
return request({
url: '/你的服务名/background/filelist/export',
method: 'get',
params: query
})
}
在app.vue中将文件的上传组件结果展示组件进行引入使用
<template>
<div id="app">
<router-view />
<theme-picker />
<!--这是重点 -->
<global-uploader></global-uploader>
</div>
</template>
<script>
import ThemePicker from "@/components/ThemePicker";
import globalUploader from '@/views/closedoff/background/fileuploader/globalUploader'
export default {
name: "App",
components: { ThemePicker,globalUploader},
metaInfo() {
return {
title: this.$store.state.settings.dynamicTitle && this.$store.state.settings.title,
titleTemplate: title => {
return title ? `${title} - ${process.env.VUE_APP_TITLE}` : process.env.VUE_APP_TITLE
}
}
}
};
</script>
<style scoped>
#app .theme-picker {
display: none;
}
</style>
后端:
1.新建bigfileupload服务,设置服务的配置文件配置rides,配置数据库(找不到就联系我)
spring配置
这是我公共配置中的值改成你的,并和下面的配置一同放到你的aplication.yml中
spring:
# 文件上传 此处是重点
servlet:
multipart:
# 单个文件大小
max-file-size: 10MB
# 设置总上传的文件大小
max-request-size: 20MB
mvc:
pathmatch:
matching-strategy: ant_path_matcher
# redis:
# host: localhost
# port: 6379
# password:你的
datasource:
druid:
stat-view-servlet:
enabled: true
loginUsername: admin
loginPassword: 123456
dynamic:
druid:
initial-size: 5
min-idle: 5
maxActive: 20
maxWait: 60000
connectTimeout: 30000
socketTimeout: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
filters: stat,slf4j
connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
datasource:
# 主库数据源
master:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/file_upload_db?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: 你的
# 从库数据源
# slave:
# username:
# password:
# url:
# driver-class-name:
# mybatis配置
mybatis:
# 搜索指定包别名
typeAliasesPackage: com.ruoyi.bigfileupload,com.ruoyi.system
# 配置mapper的扫描,找到所有的mapper.xml映射文件
mapperLocations: classpath:mapper/**/*.xml
# swagger配置
swagger:
title: 系统模块接口文档
license: Powered By ruoyi
licenseUrl: https://ruoyi.vip
配置上传的文件的本地路径与网络地址的映射(可以是文件服务器地址)
package com.ruoyi.bigfileupload.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.io.File;
/**
* 通用映射配置
*
* @author ruoyi
*/
@Configuration
public class ResourcesConfig implements WebMvcConfigurer
{
/**
* 上传文件存储在本地的根路径
*/
@Value("${file.path}")
private String localFilePath;
/**
* 资源映射路径 前缀
*/
@Value("${file.prefix}")
public String localFilePrefix;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry)
{
/** 本地文件上传路径
* 大文件文件预览地址 http://localhost:8080/bigfileupload/statics/886c22e120178aebb65e8611cd405194/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA%E5%AD%A6%E4%B9%A0webservice%E8%B5%84%E6%96%99.rar
* */
registry.addResourceHandler(localFilePrefix + "/**").addResourceLocations("file:" + localFilePath+localFilePrefix + File.separator);
}
/**
* 开启跨域
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
// 设置允许跨域的路由
registry.addMapping(localFilePrefix + "/**")
// 设置允许跨域请求的域名
.allowedOrigins("*")
// 设置允许的方法
.allowedMethods("GET");
}
}
前后端代码详情请去看源码:https://gitee.com/donghuangtaiyi/file-uploader