路由过渡动画的配置由`@/config`移动至`@/layout/store/page.js` 使用新的上传组件替换el-upload 引入了file-saver以替换原有的文件下载代码 上传文件时key的拼接加入用户id 修复列表页自适应高度的问题 代码优化 样式修改 部分文件名称修改master
parent
3e01d306c3
commit
c44753a0ce
@ -0,0 +1,17 @@ |
||||
/*仅覆盖element-ui中的预设变量,不会被使用*/ |
||||
|
||||
//返回顶部的背景色 |
||||
$--backtop-background-color: $--background-color-base; |
||||
//返回顶部hover背景色 |
||||
$--backtop-hover-background-color: $--border-color-lighter; |
||||
|
||||
//el-menu的hover背景颜色 |
||||
$--menu-item-hover-fill: none; |
||||
|
||||
//el-table的表头字体颜色 |
||||
$--table-header-font-color: $--color-text-primary; |
||||
//el-table的表头背景颜色 |
||||
$--table-header-background-color: #fbfbfb; |
||||
|
||||
//引入element的scss时需要覆盖字体路径 |
||||
$--font-path: '~element-ui/lib/theme-chalk/fonts'; |
||||
@ -0,0 +1,32 @@ |
||||
@mixin clearfix { |
||||
&::after { |
||||
visibility: hidden; |
||||
display: block; |
||||
font-size: 0; |
||||
content: " "; |
||||
clear: both; |
||||
height: 0; |
||||
} |
||||
} |
||||
|
||||
@mixin deep-shadow { |
||||
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12); |
||||
} |
||||
|
||||
@mixin mobile { |
||||
@media (max-width: $max-mobile-width) { |
||||
@content |
||||
} |
||||
} |
||||
|
||||
@mixin pc { |
||||
@media (min-width: $max-mobile-width) { |
||||
@content |
||||
} |
||||
} |
||||
|
||||
@mixin max-width-on-mobile { |
||||
@include mobile { |
||||
max-width: 384px; |
||||
} |
||||
} |
||||
@ -0,0 +1,309 @@ |
||||
<template> |
||||
<div :class="{'upload-file': true, 'is-disabled': disabled}"> |
||||
<div |
||||
v-for="file in fileList" |
||||
:key="file.id" |
||||
:class="`upload-file-item is-${file.status}`" |
||||
:title="file.name" |
||||
> |
||||
<img :src="file.thumbnailUrl"> |
||||
|
||||
<div v-if="file.status === 'uploading'" class="upload-file-item-loading"> |
||||
<i class="el-icon-loading"/> |
||||
<p>上传中</p> |
||||
</div> |
||||
|
||||
<template v-else> |
||||
<div class="upload-file-item-action"> |
||||
<template v-if="file.status === 'success'"> |
||||
<i class="el-icon-zoom-in" title="预览" @click="handlePreview(file)"/> |
||||
<i class="el-icon-download" title="下载" @click="handleDownload(file)"/> |
||||
</template> |
||||
<i v-if="!disabled" class="el-icon-delete" title="删除" @click="handleRemove(file)"/> |
||||
</div> |
||||
|
||||
<label :class="`upload-file-item-status-label`"> |
||||
<i :class="`el-icon-${file.status==='success'?'check':'close'}`"/> |
||||
</label> |
||||
</template> |
||||
</div> |
||||
|
||||
<div v-if="!hideTrigger" class="upload-file-trigger" @click="handleTriggerClick"> |
||||
<i class="el-icon-plus"></i> |
||||
<input ref="input" type="file" :multiple="multiple" :accept="accept" @change="handleInputChange"> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import ajax from 'element-ui/packages/upload/src/ajax' |
||||
|
||||
export default { |
||||
name: "UploadPictureCard", |
||||
|
||||
props: { |
||||
//上传的地址 |
||||
action: String, |
||||
//设置上传的请求头部,为函数时入参为当前上传的文件file |
||||
headers: [Object, Function], |
||||
//上传时附带的额外参数,为函数时入参为当前上传的文件file |
||||
data: [Object, Function], |
||||
//上传时是否携带cookie |
||||
withCredentials: Boolean, |
||||
//上传的文件字段名 |
||||
name: {type: String, default: 'file'}, |
||||
//发送文件上传请求的实现 |
||||
httpRequest: {type: Function, default: ajax}, |
||||
|
||||
//是否支持多选文件 |
||||
multiple: Boolean, |
||||
//接受上传的文件类型,即input的accept属性 |
||||
accept: String, |
||||
//已上传的文件列表 |
||||
uploadedFileList: {type: Array, default: () => []}, |
||||
//是否禁用 |
||||
disabled: Boolean, |
||||
//是否在禁用时隐藏上传按钮 |
||||
hideTriggerOnDisabled: {type: Boolean, default: true}, |
||||
//最大允许上传个数,<1时不作限制 |
||||
limit: {type: Number, default: 0}, |
||||
//是否在已上传个数等于最大上传个数时隐藏上传按钮 |
||||
hideTriggerOnExceed: {type: Boolean, default: true}, |
||||
//是否在文件上传失败时立即删除 |
||||
autoDelete: {type: Boolean, default: true}, |
||||
//最大可上传的文件大小,小于1时不作限制,Number型单位为B,String型后缀支持KB、MB |
||||
maxSize: {type: [Number, String], default: 0}, |
||||
|
||||
//上传文件之前的钩子,参数为上传的文件,若返回 false 或者返回 Promise 且被 reject,则停止上传 |
||||
beforeUpload: Function, |
||||
//删除文件之前的钩子,参数为上传的文件、文件列表,若返回 false 或者返回 Promise 且被 reject,则停止删除 |
||||
beforeRemove: Function |
||||
}, |
||||
|
||||
data() { |
||||
//用于确定上传文件的唯一id |
||||
this.tempId = 1 |
||||
//上传请求哈希表:<file.id, XHR> |
||||
this.requestMap = {} |
||||
|
||||
return { |
||||
//文件列表,包含已上传、未上传、正在上传、上传失败的文件 |
||||
fileList: [] |
||||
} |
||||
}, |
||||
|
||||
computed: { |
||||
needLimit() { |
||||
return this.limit >= 1 |
||||
}, |
||||
|
||||
hideTrigger() { |
||||
return this.disabled && this.hideTriggerOnDisabled |
||||
|| this.needLimit && this.fileList.length >= this.limit && this.hideTriggerOnExceed |
||||
} |
||||
}, |
||||
|
||||
watch: { |
||||
uploadedFileList: { |
||||
immediate: true, |
||||
handler(arr) { |
||||
//取消正在上传的文件 |
||||
Object.entries(this.requestMap).forEach(([id, xhr]) => { |
||||
if (arr.every(file => file.id !== id)) { |
||||
//如果是自定义的上传请求,可能没有实现abort |
||||
xhr.abort && xhr.abort() |
||||
delete this.requestMap[id] |
||||
} |
||||
}) |
||||
|
||||
this.fileList = arr.map(item => ({ |
||||
...item, |
||||
id: item.id || this.tempId++, |
||||
status: item.status || 'success', |
||||
thumbnailUrl: item.thumbnailUrl || item.url, |
||||
downloadUrl: item.thumbnailUrl || item.url |
||||
})) |
||||
} |
||||
} |
||||
}, |
||||
|
||||
methods: { |
||||
parseMaxSize(maxSize) { |
||||
if (typeof maxSize === 'number') { |
||||
return maxSize |
||||
} |
||||
|
||||
const map = {KB: 1024, MB: 1024 * 1024}, |
||||
upper = maxSize.toUpperCase(), |
||||
num = upper.replace(/[^0-9]/ig, ""), |
||||
unit = upper.replace(num, "") |
||||
|
||||
return parseFloat(num) * (map[unit] || 1) |
||||
}, |
||||
|
||||
//点击上传按钮 |
||||
handleTriggerClick() { |
||||
if (this.disabled) return |
||||
|
||||
this.$refs.input.value = null |
||||
this.$refs.input.click() |
||||
}, |
||||
//隐藏的input的值改变 |
||||
handleInputChange(e) { |
||||
const files = e.target.files |
||||
|
||||
if (!files || files.length === 0) return |
||||
|
||||
if (this.needLimit && this.fileList.length + files.length > this.limit) { |
||||
return this.$emit('exceed') |
||||
} |
||||
|
||||
files.forEach(this.upload) |
||||
}, |
||||
//点击预览按钮 |
||||
handlePreview(file) { |
||||
this.$emit('preview', file) |
||||
}, |
||||
//点击下载按钮 |
||||
handleDownload(file) { |
||||
this.$emit('download', file) |
||||
}, |
||||
//点击删除按钮 |
||||
handleRemove(file) { |
||||
this.remove(file) |
||||
}, |
||||
|
||||
//上传单个文件 |
||||
upload(rawFile) { |
||||
//判断文件大小是否超出限制 |
||||
const maxSize = this.parseMaxSize(this.maxSize) |
||||
if (maxSize >= 1 && rawFile.size > maxSize) { |
||||
return this.$emit('over-max-size', rawFile) |
||||
} |
||||
|
||||
rawFile.id = this.tempId++ |
||||
|
||||
const file = { |
||||
id: rawFile.id, |
||||
name: rawFile.name, |
||||
thumbnailUrl: rawFile.type.startsWith('image') && window.URL.createObjectURL(rawFile), |
||||
status: 'uploading', |
||||
percentage: 0, |
||||
raw: rawFile |
||||
} |
||||
|
||||
this.fileList.push(file) |
||||
|
||||
if (!this.beforeUpload) { |
||||
return this.sendRequest(file) |
||||
} |
||||
|
||||
const result = this.beforeUpload(file) |
||||
|
||||
if (!result) { |
||||
return this.removeFile(file) |
||||
} |
||||
|
||||
result.then |
||||
? result.then(() => this.sendRequest(file), () => this.removeFile(file)) |
||||
: this.sendRequest(file) |
||||
}, |
||||
sendRequest(file) { |
||||
const {id, raw} = file |
||||
const headers = typeof this.headers === 'function' ? this.headers(file) : this.headers |
||||
const data = typeof this.data === 'function' ? this.data(file) : this.data |
||||
const options = { |
||||
headers, |
||||
data, |
||||
withCredentials: this.withCredentials, |
||||
file: raw, |
||||
filename: this.name, |
||||
action: this.action, |
||||
onProgress: e => { |
||||
this.onProgress(e, file) |
||||
}, |
||||
onSuccess: res => { |
||||
this.onSuccess(res, file) |
||||
delete this.requestMap[id] |
||||
}, |
||||
onError: err => { |
||||
this.onError(err, file) |
||||
delete this.requestMap[id] |
||||
} |
||||
} |
||||
const req = this.httpRequest(options) |
||||
this.requestMap[id] = req |
||||
req && req.then && req.then(options.onSuccess, options.onError) |
||||
}, |
||||
abortRequest(file) { |
||||
const map = this.requestMap |
||||
|
||||
if (file) { |
||||
const req = map[file.id] |
||||
req && req.abort && req.abort() |
||||
delete map[file.id] |
||||
return |
||||
} |
||||
|
||||
Object.entries(map).forEach(([id, req]) => { |
||||
req && req.abort && req.abort() |
||||
delete map[id] |
||||
}) |
||||
}, |
||||
onProgress(e, file) { |
||||
file.status = 'uploading' |
||||
file.percentage = e.percent || 0 |
||||
}, |
||||
onSuccess(res, file) { |
||||
file.status = 'success' |
||||
file.response = res |
||||
this.$emit('success', res, file) |
||||
}, |
||||
onError(err, file) { |
||||
file.status = 'error' |
||||
if (!file.response) file.response = {} |
||||
file.response.err = err |
||||
this.autoDelete && this.removeFile(file) |
||||
this.$emit('error', err, file) |
||||
}, |
||||
|
||||
//删除文件 |
||||
remove(file) { |
||||
if (!this.beforeRemove) { |
||||
return this.removeFile(file) |
||||
} |
||||
|
||||
const result = this.beforeRemove(file) |
||||
|
||||
if (!result) return |
||||
|
||||
result.then |
||||
? result.then(() => this.removeFile(file), () => ({})) |
||||
: this.removeFile(file) |
||||
}, |
||||
removeFile(file) { |
||||
this.revokeBlob(file) |
||||
this.fileList.splice(this.fileList.indexOf(file), 1) |
||||
this.$emit('remove', file) |
||||
}, |
||||
revokeBlob({thumbnailUrl, downloadUrl}) { |
||||
if (thumbnailUrl && thumbnailUrl.startsWith('blob:')) { |
||||
window.URL.revokeObjectURL(thumbnailUrl) |
||||
} |
||||
if (downloadUrl && downloadUrl.startsWith('blob:')) { |
||||
window.URL.revokeObjectURL(downloadUrl) |
||||
} |
||||
} |
||||
}, |
||||
|
||||
beforeDestroy() { |
||||
//取消正在上传的文件 |
||||
this.abortRequest() |
||||
|
||||
//释放上传文件的本地缓存 |
||||
this.fileList.forEach(this.revokeBlob) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss" src="./style.scss"></style> |
||||
@ -0,0 +1,147 @@ |
||||
<template> |
||||
<upload-picture-card |
||||
ref="upload" |
||||
:uploaded-file-list="uploadedFileList" |
||||
:limit="limit" |
||||
:multiple="multiple" |
||||
:http-request="httpRequest" |
||||
:before-upload="beforeUpload" |
||||
@exceed="onExceed" |
||||
@over-max-size="onOverMaxSize" |
||||
@success="onSuccess" |
||||
@error="onError" |
||||
@preview="onPreview" |
||||
@download="onDownload" |
||||
@remove="onRemove" |
||||
/> |
||||
</template> |
||||
|
||||
<script> |
||||
/* |
||||
* 直传七牛云 |
||||
* 传入fileList自动拼接七牛云外链前缀 |
||||
* 非图片类型的文件使用kkFileView预览 |
||||
* success、remove事件中的file地址不带七牛云外链前缀 |
||||
* */ |
||||
import UploadPictureCard from "./PictureCard" |
||||
import axios from 'axios' |
||||
import {deleteUpload} from '@/api/file' |
||||
import {file as fileConfig} from '@/config' |
||||
import {elError} from "@/util/message" |
||||
import {isImage, isDoc, isPdf, isPpt, isRar, isXls, isTxt, isZip} from "@/util/validate" |
||||
import {preview, download, upload, autoCompleteUrl} from "@/util/file" |
||||
|
||||
const typeMapper = [ |
||||
{test: isImage}, {test: isDoc, type: 'doc'}, {test: isPdf, type: 'pdf'}, {test: isPpt, type: 'ppt'}, |
||||
{test: isRar, type: 'rar'}, {test: isXls, type: 'xls'}, {test: isTxt, type: 'txt'}, {test: isZip, type: 'zip'}, |
||||
] |
||||
|
||||
function getCoverImage(url, fileType) { |
||||
const mapper = typeMapper.find(({test}) => test(fileType)) |
||||
|
||||
return mapper && mapper.type ? `${process.env.BASE_URL}static/img/fileType/${mapper.type}.png` : url |
||||
} |
||||
|
||||
export default { |
||||
name: "QiniuUpload", |
||||
|
||||
components: {UploadPictureCard}, |
||||
|
||||
props: { |
||||
disabled: Boolean, |
||||
multiple: {type: Boolean, default: true}, |
||||
fileList: {type: Array, default: () => []}, |
||||
limit: {type: Number, default: 10}, |
||||
maxSize: {type: [Number, String], default: '10MB'} |
||||
}, |
||||
|
||||
computed: { |
||||
//将{url, name}转为{thumbnailUrl, downloadUrl, name}的形式 |
||||
uploadedFileList() { |
||||
return this.fileList.map(file => { |
||||
const downloadUrl = autoCompleteUrl(file.url) |
||||
return {...file, thumbnailUrl: getCoverImage(downloadUrl, file.name), downloadUrl} |
||||
}) |
||||
} |
||||
}, |
||||
|
||||
methods: { |
||||
//数量超限 |
||||
onExceed() { |
||||
elError(`最多只能上传${this.limit}个文件`) |
||||
}, |
||||
//文件大小超限 |
||||
onOverMaxSize(file) { |
||||
const maxSize = this.$refs.upload.parseMaxSize(file.size) |
||||
elError(`${file.name}的大小超出${this.$options.filters.number2StorageUnit(maxSize)}`) |
||||
}, |
||||
//新附件上传成功,触发success事件,记录文件key |
||||
onSuccess(res, file) { |
||||
file.raw.key = res.key |
||||
file.downloadUrl = autoCompleteUrl(res.key) |
||||
this.$emit('success', file, res) |
||||
}, |
||||
//上传失败 |
||||
onError(err) { |
||||
elError('上传文件失败:' + err) |
||||
}, |
||||
//预览 |
||||
onPreview(file) { |
||||
if (!isImage(file.name)) return preview(file.downloadUrl) |
||||
|
||||
const fileList = this.$refs.upload.fileList |
||||
const index = fileList.indexOf(file) |
||||
const previewUrlList = fileList.filter(i => isImage(i.name)).map(i => i.downloadUrl) |
||||
this.$image({index, urlList: previewUrlList}) |
||||
}, |
||||
//下载 |
||||
onDownload(file) { |
||||
download(file.downloadUrl, file.name) |
||||
}, |
||||
//移除 |
||||
onRemove(file) { |
||||
//若不是新上传的,触发remove事件 |
||||
if (!file.raw) { |
||||
this.$emit('remove', {url: file.downloadUrl.replace(fileConfig.storePrefix, '')}) |
||||
} |
||||
//移除本次新上传的文件时,删除OSS上的文件 |
||||
else if (file.response && !file.response.err) { |
||||
deleteUpload.request(file.raw.key) |
||||
} |
||||
}, |
||||
|
||||
//上传前过滤文件,并根据文件类型修改缩略图 |
||||
beforeUpload(file) { |
||||
if (!typeMapper.some(({test}) => test(file.name))) { |
||||
elError('只支持图片、word、pdf、ppt、rar、excel、txt、zip类型的文件') |
||||
return false |
||||
} |
||||
|
||||
const url = getCoverImage(file.thumbnailUrl, file.name) |
||||
|
||||
//修改非图片类型文件的缩略图 |
||||
if (url !== file.thumbnailUrl) { |
||||
file.thumbnailUrl = url |
||||
} |
||||
|
||||
return true |
||||
}, |
||||
//覆盖默认的上传行为 |
||||
httpRequest({file, onProgress}) { |
||||
const source = axios.CancelToken.source() |
||||
const promise = upload(file, file.name, { |
||||
onUploadProgress(e) { |
||||
if (e.total > 0) { |
||||
e.percent = (Number)((e.loaded / e.total * 100).toFixed(2)) |
||||
} |
||||
onProgress(e) |
||||
}, |
||||
cancelToken: source.token |
||||
}) |
||||
//取消上传时会调用一次abort方法 |
||||
promise.abort = source.cancel |
||||
return promise |
||||
} |
||||
} |
||||
} |
||||
</script> |
||||
@ -0,0 +1,135 @@ |
||||
@import "~@/asset/style/var"; |
||||
|
||||
$--upload-size: 90px; |
||||
|
||||
.upload-file { |
||||
display: inline-block; |
||||
|
||||
&-item, |
||||
&-trigger { |
||||
height: $--upload-size; |
||||
width: $--upload-size; |
||||
float: left; |
||||
border-radius: 4px; |
||||
} |
||||
|
||||
&-item { |
||||
position: relative; |
||||
margin: 0 8px 8px 0; |
||||
border: 1px solid $--border-color-base; |
||||
overflow: hidden; |
||||
|
||||
> img { |
||||
height: 100%; |
||||
width: 100%; |
||||
object-fit: cover; |
||||
} |
||||
|
||||
&-action, |
||||
&-loading { |
||||
position: absolute; |
||||
top: 0; |
||||
width: 100%; |
||||
height: 100%; |
||||
background-color: rgba(0, 0, 0, .6); |
||||
} |
||||
|
||||
&-action { |
||||
display: flex; |
||||
justify-content: space-around; |
||||
align-items: center; |
||||
opacity: 0; |
||||
|
||||
> i { |
||||
color: $--color-white; |
||||
cursor: pointer; |
||||
transition: transform .1s; |
||||
|
||||
&:hover { |
||||
transform: scale(1.2); |
||||
} |
||||
} |
||||
} |
||||
|
||||
&-loading { |
||||
padding-top: ($--upload-size - $--font-size-small * 2 - $--font-size-extra-large) / 2; |
||||
text-align: center; |
||||
color: $--color-white; |
||||
|
||||
> i { |
||||
font-size: $--font-size-extra-large; |
||||
} |
||||
|
||||
> p { |
||||
font-size: $--font-size-small; |
||||
} |
||||
} |
||||
|
||||
&-status-label { |
||||
position: absolute; |
||||
right: -15px; |
||||
top: -6px; |
||||
width: 40px; |
||||
height: 24px; |
||||
text-align: center; |
||||
transform: rotate(45deg); |
||||
|
||||
> i { |
||||
font-size: 12px; |
||||
color: $--color-white; |
||||
margin-top: 11px; |
||||
transform: rotate(-45deg); |
||||
} |
||||
} |
||||
|
||||
&.is-success > &-status-label { |
||||
background-color: $--color-success; |
||||
} |
||||
|
||||
&.is-error > &-status-label { |
||||
background-color: $--color-danger; |
||||
} |
||||
|
||||
&:hover { |
||||
> .upload-file-item-action { |
||||
opacity: 1; |
||||
} |
||||
|
||||
> .upload-file-item-status-label { |
||||
display: none; |
||||
} |
||||
} |
||||
} |
||||
|
||||
&-trigger { |
||||
display: flex; |
||||
flex-direction: column; |
||||
justify-content: center; |
||||
align-items: center; |
||||
border: 1px dashed $--border-color-base; |
||||
cursor: pointer; |
||||
background-color: $--background-color-base; |
||||
|
||||
> i { |
||||
font-size: 20px; |
||||
color: $--color-text-regular; |
||||
} |
||||
|
||||
> input { |
||||
display: none; |
||||
} |
||||
|
||||
&:hover { |
||||
border-color: $--color-primary; |
||||
} |
||||
} |
||||
|
||||
&.is-disabled > &-trigger { |
||||
border-color: $--disabled-border-base; |
||||
cursor: not-allowed; |
||||
|
||||
&:hover { |
||||
border-color: $--disabled-border-base; |
||||
} |
||||
} |
||||
} |
||||
@ -1,242 +0,0 @@ |
||||
<template> |
||||
<el-upload |
||||
ref="upload" |
||||
:before-upload="beforeUpload" |
||||
:class="{disabled: hideUploader}" |
||||
:file-list="tempData" |
||||
:http-request="httpRequest" |
||||
:limit="limit" |
||||
:multiple="multiple" |
||||
:on-error="error" |
||||
:on-exceed="exceed" |
||||
:on-success="success" |
||||
action="" |
||||
list-type="picture-card" |
||||
> |
||||
<i class="el-icon-plus"/> |
||||
|
||||
<template v-slot:file="{file}"> |
||||
<span |
||||
class="el-upload-list__item-actions" |
||||
@mouseenter="e => handleBlockMouseEnter(e,file)" |
||||
@mouseleave="handleBlockMouseLeave" |
||||
> |
||||
<span class="el-upload-list__item-preview" @click="() => preview(file)"> |
||||
<i class="el-icon-zoom-in"/> |
||||
</span> |
||||
<span class="el-upload-list__item-delete" @click="() => download(file)"> |
||||
<i class="el-icon-download"/> |
||||
</span> |
||||
<span v-if="!disabled" class="el-upload-list__item-delete" @click="() => remove(file)"> |
||||
<i class="el-icon-delete"/> |
||||
</span> |
||||
</span> |
||||
|
||||
<template v-if="file.status === 'success'"> |
||||
<img :src="file.url" class="el-upload-list__item-thumbnail"> |
||||
<label class="el-upload-list__item-status-label"> |
||||
<i class="el-icon-upload-success el-icon-check"/> |
||||
</label> |
||||
</template> |
||||
|
||||
<div v-if="file.status === 'uploading'" class="progress-mask"> |
||||
<el-progress :percentage="file.percentage" type="circle"/> |
||||
</div> |
||||
</template> |
||||
|
||||
<el-tooltip ref="tooltip" :content="tooltipContent" popper-class="upload-tooltip"/> |
||||
</el-upload> |
||||
</template> |
||||
|
||||
<script> |
||||
/* |
||||
* 直传七牛云 |
||||
* 传入fileList自动拼接七牛云外链前缀,并增加downloadUrl属性(图片类型的文件url与downloadUrl相同) |
||||
* 非图片类型的文件使用kkFileView预览 |
||||
* success、remove事件中的file.url均不带七牛云外链前缀 |
||||
* */ |
||||
import axios from 'axios' |
||||
import {deleteUpload} from '@/api/file' |
||||
import {file as fileConfig} from '@/config' |
||||
import {debounce, isEmpty, deepClone} from '@/util' |
||||
import {elError} from "@/util/message" |
||||
import {isImage, isDoc, isPdf, isPpt, isRar, isXls, isTxt, isZip} from "@/util/validate" |
||||
import {preview, download, upload, autoCompleteUrl} from "@/util/file" |
||||
|
||||
const typeMapper = [ |
||||
{test: isImage}, {test: isDoc, type: 'doc'}, {test: isPdf, type: 'pdf'}, {test: isPpt, type: 'ppt'}, |
||||
{test: isRar, type: 'rar'}, {test: isXls, type: 'xls'}, {test: isTxt, type: 'txt'}, {test: isZip, type: 'zip'}, |
||||
] |
||||
|
||||
function parseMaxSize(maxSize) { |
||||
if (typeof maxSize === 'number') { |
||||
return maxSize |
||||
} |
||||
|
||||
const map = {KB: 1024, MB: 1024 * 1024}, |
||||
upper = maxSize.toUpperCase(), |
||||
num = upper.replace(/[^0-9]/ig, ""), |
||||
unit = upper.replace(num, "") |
||||
|
||||
return parseInt(num) * (map[unit] || 1) |
||||
} |
||||
|
||||
function getCoverImage(url, fileType) { |
||||
const mapper = typeMapper.find(({test}) => test(fileType)) |
||||
|
||||
return mapper && mapper.type ? `${process.env.BASE_URL}static/img/fileType/${mapper.type}.png` : url |
||||
} |
||||
|
||||
export default { |
||||
name: 'UploadFile', |
||||
|
||||
props: { |
||||
disabled: Boolean, |
||||
multiple: {type: Boolean, default: true}, |
||||
fileList: {type: Array, default: () => []}, |
||||
limit: {type: Number, default: 10}, |
||||
maxSize: {type: [Number, String], default: '10MB'} |
||||
}, |
||||
|
||||
data() { |
||||
return { |
||||
tempData: this.fileList, |
||||
data: this.fileList, |
||||
tooltipContent: null |
||||
} |
||||
}, |
||||
|
||||
computed: { |
||||
count() { |
||||
return this.data.length |
||||
}, |
||||
hideUploader() { |
||||
return this.disabled || this.count >= this.limit |
||||
}, |
||||
previewUrlList() { |
||||
return this.data ? this.data.map(i => i.downloadUrl) : [] |
||||
} |
||||
}, |
||||
|
||||
watch: { |
||||
fileList: { |
||||
immediate: true, |
||||
handler(fileList) { |
||||
if (isEmpty(fileList)) this.data = [] |
||||
else { |
||||
this.data = [...this.fileList.map(file => { |
||||
const downloadUrl = autoCompleteUrl(file.url) |
||||
return {...file, url: getCoverImage(downloadUrl, file.name), downloadUrl} |
||||
})] |
||||
} |
||||
this.tempData = deepClone(this.data) |
||||
} |
||||
} |
||||
}, |
||||
|
||||
methods: { |
||||
//模仿el-table的tooltip |
||||
handleBlockMouseEnter(event, file) { |
||||
const tooltip = this.$refs.tooltip |
||||
this.tooltipContent = file.name |
||||
tooltip.referenceElm = event.target |
||||
tooltip.$refs.popper && (tooltip.$refs.popper.style.display = 'none') |
||||
tooltip.doDestroy() |
||||
tooltip.setExpectedState(true) |
||||
this.activateTooltip(tooltip) |
||||
}, |
||||
handleBlockMouseLeave() { |
||||
const tooltip = this.$refs.tooltip |
||||
if (tooltip) { |
||||
tooltip.setExpectedState(false) |
||||
tooltip.handleClosePopper() |
||||
} |
||||
}, |
||||
|
||||
//附件移除时,仅当非本次上传时触发remove事件 |
||||
remove(file) { |
||||
//删除上传文件时隐藏tooltip |
||||
this.handleBlockMouseLeave() |
||||
|
||||
//区分是否为本次新上传,以及是否上传成功 |
||||
//若不是新上传的 |
||||
if (!file.raw) this.$emit('remove', {url: file.downloadUrl.replace(fileConfig.storePrefix, '')}) |
||||
//判断是否上传成功(有可能是正在上传) |
||||
else if (file.response && !file.response.err) deleteUpload.request(file.raw.key) |
||||
|
||||
this.$refs.upload.handleRemove(file) |
||||
const index = this.data.findIndex(i => i.downloadUrl === file.downloadUrl) |
||||
if (index > -1) this.data.splice(index, 1) |
||||
}, |
||||
|
||||
//预览 |
||||
preview(file) { |
||||
if (!isImage(file.name)) return preview(file.downloadUrl) |
||||
|
||||
const index = this.data.findIndex(i => i.downloadUrl === file.downloadUrl) |
||||
this.$image({index, urlList: this.previewUrlList}) |
||||
}, |
||||
|
||||
//下载 |
||||
download(file) { |
||||
download(file.downloadUrl, file.name) |
||||
}, |
||||
|
||||
//数量超限 |
||||
exceed() { |
||||
elError(`最多只能上传${this.limit}个文件`) |
||||
}, |
||||
|
||||
//新附件上传成功,触发success事件,记录文件key |
||||
success(res, file) { |
||||
file.url = getCoverImage(file.url, file.name) |
||||
file.raw.key = res.key |
||||
file.downloadUrl = autoCompleteUrl(res.key) |
||||
this.data.push(file) |
||||
this.$emit('success', file, res) |
||||
}, |
||||
|
||||
//上传失败时移除文件 |
||||
error(err, file) { |
||||
elError('上传文件失败,' + err) |
||||
file.response.err = err |
||||
this.remove(file) |
||||
}, |
||||
|
||||
//上传前过滤文件 |
||||
beforeUpload(file) { |
||||
if (!typeMapper.some(({test}) => test(file.name))) { |
||||
elError('只支持图片、word、pdf、ppt、rar、excel、txt、zip类型的文件') |
||||
return false |
||||
} |
||||
const maxSize = parseMaxSize(this.maxSize) |
||||
if (file.size > maxSize) { |
||||
elError(`${file.name}的大小超出${this.$options.filters.number2StorageUnit(maxSize)}`) |
||||
return false |
||||
} |
||||
}, |
||||
|
||||
httpRequest({file, onProgress}) { |
||||
const source = axios.CancelToken.source() |
||||
const promise = upload(file, file.name, { |
||||
onUploadProgress(e) { |
||||
if (e.total > 0) { |
||||
e.percent = (Number)((e.loaded / e.total * 100).toFixed(2)) |
||||
} |
||||
onProgress(e) |
||||
}, |
||||
cancelToken: source.token |
||||
}) |
||||
//由于ele原有的上传方法是原生ajax,所以取消上传时会调用一次abort方法 |
||||
promise.abort = source.cancel |
||||
return promise |
||||
} |
||||
}, |
||||
|
||||
created() { |
||||
this.activateTooltip = debounce(tooltip => tooltip.handleShowPopper(), 200) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss" src="./style.scss"></style> |
||||
@ -1,34 +0,0 @@ |
||||
@import "~@/asset/style/variables.scss"; |
||||
|
||||
.disabled .el-upload--picture-card { |
||||
display: none; |
||||
} |
||||
|
||||
.el-upload-list--picture-card { |
||||
.el-progress { |
||||
width: 100%; |
||||
height: 100%; |
||||
|
||||
.el-progress-circle { |
||||
height: 100% !important; |
||||
width: 100% !important; |
||||
} |
||||
} |
||||
|
||||
.progress-mask { |
||||
position: absolute; |
||||
top: 0; |
||||
height: 100%; |
||||
width: 100%; |
||||
background-color: #ffffff; |
||||
opacity: 0.9; |
||||
|
||||
.el-progress__text { |
||||
color: $--color-primary; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.upload-tooltip { |
||||
max-width: 146px; |
||||
} |
||||
@ -1,11 +1,11 @@ |
||||
.navbar.light { |
||||
background-color: #ffffff; |
||||
background-color: $--color-white; |
||||
|
||||
.navbar-item { |
||||
color: $navbar-item-color-light; |
||||
|
||||
&:hover { |
||||
background: $navbar-item-hover-color-light; |
||||
background-color: $navbar-item-hover-color-light; |
||||
} |
||||
} |
||||
} |
||||
|
||||
@ -0,0 +1,23 @@ |
||||
@import "~@/asset/style/var"; |
||||
|
||||
.page-main { |
||||
display: flex; |
||||
flex-direction: column; |
||||
flex: 1; |
||||
position: relative; |
||||
overflow: hidden; |
||||
background-color: #f0f2f5; |
||||
|
||||
> .scroll-container { |
||||
display: flex; |
||||
flex: 1; |
||||
flex-direction: column; |
||||
overflow-y: overlay; |
||||
overflow-x: inherit; |
||||
|
||||
> .page-view { |
||||
margin: $page-view-margin; |
||||
flex: 1; |
||||
} |
||||
} |
||||
} |
||||
@ -1,7 +0,0 @@ |
||||
import component from '@/view/dev/uploadFileDev' |
||||
|
||||
export default { |
||||
path: 'uploadFile', |
||||
component, |
||||
meta: {title: '上传'} |
||||
} |
||||
@ -1,11 +1,8 @@ |
||||
/*顶级菜单:开发用的页面*/ |
||||
import {context2array} from '@/router/util' |
||||
|
||||
const files = require.context('./child', false, /\.js$/) |
||||
import component from "@/view/dev" |
||||
|
||||
export default { |
||||
path: '/dev', |
||||
component: {template: `<div><div>开发专用页面</div><router-view/></div>`}, |
||||
meta: {title: '开发专用页面', icon: 'svg-develop', noAuth: true, noCache: true, alwaysShow: true}, |
||||
children: context2array(files) |
||||
component, |
||||
meta: {title: '开发专用页面', icon: 'svg-develop', noAuth: true, noCache: true, alwaysShow: true} |
||||
} |
||||
|
||||
Loading…
Reference in new issue