5 changed files with 468 additions and 2 deletions
@ -0,0 +1,370 @@
|
||||
<template> |
||||
<div ref="containerRef" :class="`${prefixCls}-container`"> |
||||
<Upload |
||||
:headers="headers" |
||||
:multiple="multiple" |
||||
:action="(uploadUrl as any)" |
||||
:fileList="fileList" |
||||
:disabled="disabled" |
||||
v-bind="bindProps" |
||||
@remove="onRemove" |
||||
@change="onFileChange" |
||||
@preview="onFilePreview" |
||||
> |
||||
<template v-if="isImageMode"> |
||||
<div v-if="!isMaxCount"> |
||||
<Icon icon="ant-design:plus-outlined" /> |
||||
<div class="ant-upload-text">{{ text }}</div> |
||||
</div> |
||||
</template> |
||||
<a-button v-else-if="buttonVisible" :disabled="isMaxCount || disabled"> |
||||
<Icon icon="ant-design:upload-outlined" /> |
||||
<span>{{ text }}</span> |
||||
</a-button> |
||||
</Upload> |
||||
</div> |
||||
</template> |
||||
|
||||
<script lang="ts" setup name="FileUpload"> |
||||
import { Upload } from 'ant-design-vue' |
||||
import { ref, reactive, computed, watch, unref } from 'vue' |
||||
import { Icon } from '@/components/Icon' |
||||
import { getAccessToken, getTenantId } from '@/utils/auth' |
||||
import { propTypes } from '@/utils/propTypes' |
||||
import { useMessage } from '@/hooks/web/useMessage' |
||||
import { createImgPreview } from '@/components/Preview/index' |
||||
import { useAttrs } from '@/hooks/core/useAttrs' |
||||
import { useDesign } from '@/hooks/web/useDesign' |
||||
import { useGlobSetting } from '@/hooks/setting' |
||||
|
||||
const { createMessage, createConfirm } = useMessage() |
||||
const { prefixCls } = useDesign('upload') |
||||
const attrs = useAttrs() |
||||
const emit = defineEmits(['change', 'update:value']) |
||||
const props = defineProps({ |
||||
value: propTypes.oneOfType([propTypes.string, propTypes.array]), |
||||
text: propTypes.string.def('上传'), |
||||
fileType: propTypes.oneOf(['all', 'image', 'file']).def('all'), |
||||
// eslint-disable-next-line vue/valid-define-props |
||||
uploadUrl: propTypes.string.def(useGlobSetting().uploadUrl), |
||||
/*这个属性用于控制文件上传的业务路径*/ |
||||
bizPath: propTypes.string.def('temp'), |
||||
/** |
||||
* 是否返回url, |
||||
* true:仅返回url |
||||
* false:返回fileName filePath fileSize |
||||
*/ |
||||
returnUrl: propTypes.bool.def(true), |
||||
// 最大上传数量 |
||||
maxCount: propTypes.number.def(0), |
||||
buttonVisible: propTypes.bool.def(true), |
||||
multiple: propTypes.bool.def(true), |
||||
// 是否显示左右移动按钮 |
||||
mover: propTypes.bool.def(true), |
||||
// 是否显示下载按钮 |
||||
download: propTypes.bool.def(true), |
||||
// 删除时是否显示确认框 |
||||
removeConfirm: propTypes.bool.def(false), |
||||
beforeUpload: propTypes.func, |
||||
disabled: propTypes.bool.def(false) |
||||
}) |
||||
|
||||
const headers = reactive({ |
||||
Authorization: 'Bearer ' + getAccessToken(), |
||||
'tenant-id': getTenantId() |
||||
}) |
||||
const fileList = ref<any[]>([]) |
||||
const uploadGoOn = ref<boolean>(true) |
||||
// refs |
||||
const containerRef = ref() |
||||
// 是否达到了最大上传数量 |
||||
const isMaxCount = computed(() => props.maxCount > 0 && fileList.value.length >= props.maxCount) |
||||
// 当前是否是上传图片模式 |
||||
const isImageMode = computed(() => props.fileType === 'image') |
||||
// 合并 props 和 attrs |
||||
const bindProps = computed(() => { |
||||
//update-begin-author:liusq date:20220411 for: [issue/455]上传组件传入accept限制上传文件类型无效 |
||||
const bind: any = Object.assign({}, props, unref(attrs)) |
||||
//update-end-author:liusq date:20220411 for: [issue/455]上传组件传入accept限制上传文件类型无效 |
||||
|
||||
bind.name = 'file' |
||||
bind.listType = isImageMode.value ? 'picture-card' : 'text' |
||||
bind.class = [bind.class, { 'upload-disabled': props.disabled }] |
||||
bind.data = { biz: props.bizPath, ...bind.data } |
||||
//update-begin-author:taoyan date:20220407 for: 自定义beforeUpload return false,并不能中断上传过程 |
||||
if (!bind.beforeUpload) { |
||||
bind.beforeUpload = onBeforeUpload |
||||
} |
||||
//update-end-author:taoyan date:20220407 for: 自定义beforeUpload return false,并不能中断上传过程 |
||||
// 如果当前是图片上传模式,就只能上传图片 |
||||
if (isImageMode.value && !bind.accept) { |
||||
bind.accept = 'image/*' |
||||
} |
||||
return bind |
||||
}) |
||||
|
||||
watch( |
||||
() => props.value, |
||||
(val) => { |
||||
if (Array.isArray(val)) { |
||||
if (props.returnUrl) { |
||||
parsePathsValue(val.join(',')) |
||||
} else { |
||||
parseArrayValue(val) |
||||
} |
||||
} else { |
||||
parsePathsValue(val) |
||||
} |
||||
}, |
||||
{ immediate: true } |
||||
) |
||||
|
||||
// 解析数据库存储的逗号分割 |
||||
function parsePathsValue(paths) { |
||||
if (!paths || paths.length == 0) { |
||||
fileList.value = [] |
||||
return |
||||
} |
||||
let list: any[] = [] |
||||
for (const item of paths.split(',')) { |
||||
list.push({ |
||||
uid: uidGenerator(), |
||||
name: getFileName(item), |
||||
status: 'done', |
||||
url: item, |
||||
response: { status: 'history', message: item } |
||||
}) |
||||
} |
||||
fileList.value = list |
||||
} |
||||
|
||||
// 解析数组值 |
||||
function parseArrayValue(array) { |
||||
if (!array || array.length == 0) { |
||||
fileList.value = [] |
||||
return |
||||
} |
||||
let list: any[] = [] |
||||
for (const item of array) { |
||||
list.push({ |
||||
uid: uidGenerator(), |
||||
name: item.fileName, |
||||
url: item.filePath, |
||||
status: 'done', |
||||
response: { status: 'history', message: item.filePath } |
||||
}) |
||||
} |
||||
fileList.value = list |
||||
} |
||||
|
||||
// 文件上传之前的操作 |
||||
function onBeforeUpload(file) { |
||||
uploadGoOn.value = true |
||||
if (isImageMode.value) { |
||||
if (file.type.indexOf('image') < 0) { |
||||
createMessage.warning('请上传图片') |
||||
uploadGoOn.value = false |
||||
return false |
||||
} |
||||
} |
||||
// 扩展 beforeUpload 验证 |
||||
if (typeof props.beforeUpload === 'function') { |
||||
return props.beforeUpload(file) |
||||
} |
||||
return true |
||||
} |
||||
|
||||
// 删除处理事件 |
||||
function onRemove() { |
||||
if (props.removeConfirm) { |
||||
return new Promise((resolve) => { |
||||
createConfirm({ |
||||
title: '删除', |
||||
content: `确定要删除这${isImageMode.value ? '张图片' : '个文件'}吗?`, |
||||
iconType: 'warning', |
||||
onOk: () => resolve(true), |
||||
onCancel: () => resolve(false) |
||||
}) |
||||
}) |
||||
} |
||||
return true |
||||
} |
||||
|
||||
// upload组件change事件 |
||||
function onFileChange(info) { |
||||
if (!info.file.status && uploadGoOn.value === false) { |
||||
info.fileList.pop() |
||||
} |
||||
let fileListTemp = info.fileList |
||||
// 限制最大上传数 |
||||
if (props.maxCount > 0) { |
||||
let count = fileListTemp.length |
||||
if (count >= props.maxCount) { |
||||
let diffNum = props.maxCount - fileListTemp.length |
||||
if (diffNum >= 0) { |
||||
fileListTemp = fileListTemp.slice(-props.maxCount) |
||||
} else { |
||||
return |
||||
} |
||||
} |
||||
} |
||||
if (info.file.status === 'done') { |
||||
if (info.file.response.success) { |
||||
fileListTemp = fileListTemp.map((file) => { |
||||
if (file.response) { |
||||
let reUrl = file.response.message |
||||
file.url = reUrl |
||||
} |
||||
return file |
||||
}) |
||||
} |
||||
} else if (info.file.status === 'error') { |
||||
createMessage.error(`${info.file.name} 上传失败.`) |
||||
} |
||||
fileList.value = fileListTemp |
||||
if (info.file.status === 'done' || info.file.status === 'removed') { |
||||
//returnUrl为true时仅返回文件路径 |
||||
if (props.returnUrl) { |
||||
handlePathChange() |
||||
} else { |
||||
//returnUrl为false时返回文件名称、文件路径及文件大小 |
||||
let newFileList: any[] = [] |
||||
for (const item of fileListTemp) { |
||||
if (item.status === 'done') { |
||||
let fileJson = { |
||||
fileName: item.name, |
||||
filePath: item.response.message, |
||||
fileSize: item.size |
||||
} |
||||
newFileList.push(fileJson) |
||||
} else { |
||||
return |
||||
} |
||||
} |
||||
emitValue(newFileList) |
||||
} |
||||
} |
||||
} |
||||
|
||||
function handlePathChange() { |
||||
let uploadFiles = fileList.value |
||||
let path = '' |
||||
if (!uploadFiles || uploadFiles.length == 0) { |
||||
path = '' |
||||
} |
||||
let pathList: string[] = [] |
||||
for (const item of uploadFiles) { |
||||
if (item.status === 'done') { |
||||
pathList.push(item.response.data) |
||||
} else { |
||||
return |
||||
} |
||||
} |
||||
if (pathList.length > 0) { |
||||
path = pathList.join(',') |
||||
} |
||||
emitValue(path) |
||||
} |
||||
|
||||
// 预览文件、图片 |
||||
function onFilePreview(file) { |
||||
if (isImageMode.value) { |
||||
createImgPreview({ imageList: [file.url], maskClosable: true }) |
||||
} else { |
||||
window.open(file.url) |
||||
} |
||||
} |
||||
|
||||
function emitValue(value) { |
||||
emit('change', value) |
||||
emit('update:value', value) |
||||
} |
||||
|
||||
function uidGenerator() { |
||||
return '-' + parseInt(Math.random() * 10000 + 1, 10) |
||||
} |
||||
|
||||
function getFileName(path) { |
||||
if (path.lastIndexOf('\\') >= 0) { |
||||
let reg = new RegExp('\\\\', 'g') |
||||
path = path.replace(reg, '/') |
||||
} |
||||
return path.substring(path.lastIndexOf('/') + 1) |
||||
} |
||||
</script> |
||||
|
||||
<style lang="less"> |
||||
//noinspection LessUnresolvedVariable |
||||
@prefix-cls: ~'@{namespace}-upload'; |
||||
|
||||
.@{prefix-cls} { |
||||
&-container { |
||||
position: relative; |
||||
|
||||
.upload-disabled { |
||||
.ant-upload-list-item { |
||||
.anticon-close { |
||||
display: none; |
||||
} |
||||
|
||||
.anticon-delete { |
||||
display: none; |
||||
} |
||||
} |
||||
|
||||
/* update-begin-author:taoyan date:2022-5-24 for:VUEN-1093详情界面 图片下载按钮显示不全 */ |
||||
.upload-download-handler { |
||||
right: 6px !important; |
||||
} |
||||
|
||||
/* update-end-author:taoyan date:2022-5-24 for:VUEN-1093详情界面 图片下载按钮显示不全 */ |
||||
} |
||||
|
||||
.ant-upload-list-item { |
||||
.upload-actions-container { |
||||
position: absolute; |
||||
top: -31px; |
||||
left: -18px; |
||||
z-index: 11; |
||||
width: 84px; |
||||
height: 84px; |
||||
line-height: 28px; |
||||
text-align: center; |
||||
pointer-events: none; |
||||
|
||||
a { |
||||
opacity: 0.9; |
||||
margin: 0 5px; |
||||
cursor: pointer; |
||||
transition: opacity 0.3s; |
||||
|
||||
.anticon { |
||||
color: #fff; |
||||
font-size: 16px; |
||||
} |
||||
|
||||
&:hover { |
||||
opacity: 1; |
||||
} |
||||
} |
||||
|
||||
.upload-mover-handler, |
||||
.upload-download-handler { |
||||
position: absolute; |
||||
pointer-events: auto; |
||||
} |
||||
|
||||
.upload-mover-handler { |
||||
width: 100%; |
||||
bottom: 0; |
||||
} |
||||
|
||||
.upload-download-handler { |
||||
top: -4px; |
||||
right: -4px; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,90 @@
|
||||
<template> |
||||
<div v-show="download" class="upload-download-handler"> |
||||
<a class="download" title="下载" @click="onDownload"> |
||||
<Icon icon="ant-design:download" /> |
||||
</a> |
||||
</div> |
||||
<div v-show="mover && list.length > 1" class="upload-mover-handler"> |
||||
<a title="向前移动" @click="onMoveForward"> |
||||
<Icon icon="ant-design:arrow-left" /> |
||||
</a> |
||||
<a title="向后移动" @click="onMoveBack"> |
||||
<Icon icon="ant-design:arrow-right" /> |
||||
</a> |
||||
</div> |
||||
</template> |
||||
|
||||
<script lang="ts" setup> |
||||
import { unref, computed } from 'vue' |
||||
import { Icon } from '@/components/Icon' |
||||
import { useMessage } from '@/hooks/web/useMessage' |
||||
|
||||
const { createMessage } = useMessage() |
||||
|
||||
const props = defineProps({ |
||||
element: { type: HTMLElement, required: true }, |
||||
fileList: { type: Object, required: true }, |
||||
mover: { type: Boolean, required: true }, |
||||
download: { type: Boolean, required: true }, |
||||
emitValue: { type: Function, required: true } |
||||
}) |
||||
const list = computed(() => unref(props.fileList)) |
||||
|
||||
// 向前移动图片 |
||||
function onMoveForward() { |
||||
let index = getIndexByUrl() |
||||
if (index === -1) { |
||||
createMessage.warn('移动失败:' + index) |
||||
return |
||||
} |
||||
if (index === 0) { |
||||
doSwap(index, unref(list).length - 1) |
||||
return |
||||
} |
||||
doSwap(index, index - 1) |
||||
} |
||||
|
||||
// 向后移动图片 |
||||
function onMoveBack() { |
||||
let index = getIndexByUrl() |
||||
if (index === -1) { |
||||
createMessage.warn('移动失败:' + index) |
||||
return |
||||
} |
||||
if (index == unref(list).length - 1) { |
||||
doSwap(index, 0) |
||||
return |
||||
} |
||||
doSwap(index, index + 1) |
||||
} |
||||
|
||||
function doSwap(oldIndex, newIndex) { |
||||
if (oldIndex !== newIndex) { |
||||
let array: any[] = [...(unref(list) as Array<any>)] |
||||
let temp = array[oldIndex] |
||||
array[oldIndex] = array[newIndex] |
||||
array[newIndex] = temp |
||||
props.emitValue(array.map((i) => i.url).join(',')) |
||||
} |
||||
} |
||||
|
||||
function getIndexByUrl() { |
||||
const url = props.element?.getElementsByTagName('img')[0]?.src |
||||
if (url) { |
||||
const fileList: any = unref(list) |
||||
for (let i = 0; i < fileList.length; i++) { |
||||
let current = fileList[i].url |
||||
const replace = url.replace(window.location.origin, '') |
||||
if (current === replace || encodeURI(current) === replace) { |
||||
return i |
||||
} |
||||
} |
||||
} |
||||
return -1 |
||||
} |
||||
|
||||
function onDownload() { |
||||
const url = props.element?.getElementsByTagName('img')[0]?.src |
||||
window.open(url) |
||||
} |
||||
</script> |
Reference in new issue