路由过渡动画的配置由`@/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 { |
.navbar.light { |
||||||
background-color: #ffffff; |
background-color: $--color-white; |
||||||
|
|
||||||
.navbar-item { |
.navbar-item { |
||||||
color: $navbar-item-color-light; |
color: $navbar-item-color-light; |
||||||
|
|
||||||
&:hover { |
&: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' |
import component from "@/view/dev" |
||||||
|
|
||||||
const files = require.context('./child', false, /\.js$/) |
|
||||||
|
|
||||||
export default { |
export default { |
||||||
path: '/dev', |
path: '/dev', |
||||||
component: {template: `<div><div>开发专用页面</div><router-view/></div>`}, |
component, |
||||||
meta: {title: '开发专用页面', icon: 'svg-develop', noAuth: true, noCache: true, alwaysShow: true}, |
meta: {title: '开发专用页面', icon: 'svg-develop', noAuth: true, noCache: true, alwaysShow: true} |
||||||
children: context2array(files) |
|
||||||
} |
} |
||||||
|
|||||||
Loading…
Reference in new issue