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