feat: file upload
parent
e63d8dbcb9
commit
611da354a1
|
@ -33,6 +33,7 @@ import { StrengthMeter } from '@/components/StrengthMeter'
|
|||
import { IconPicker } from '@/components/Icon'
|
||||
import { CountdownInput } from '@/components/CountDown'
|
||||
import { Tinymce } from '@/components/Tinymce'
|
||||
import FileUpload from './components/FileUpload.vue'
|
||||
|
||||
const componentMap = new Map<ComponentType, Component>()
|
||||
|
||||
|
@ -71,6 +72,7 @@ componentMap.set('IconPicker', IconPicker)
|
|||
componentMap.set('InputCountDown', CountdownInput)
|
||||
|
||||
componentMap.set('Upload', BasicUpload)
|
||||
componentMap.set('FileUpload', FileUpload)
|
||||
componentMap.set('Divider', Divider)
|
||||
componentMap.set('Editor', Tinymce)
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -116,3 +116,4 @@ export type ComponentType =
|
|||
| 'Divider'
|
||||
| 'ApiTransfer'
|
||||
| 'Editor'
|
||||
| 'FileUpload'
|
||||
|
|
|
@ -110,12 +110,15 @@ export const formSchema: FormSchema[] = [
|
|||
required: true,
|
||||
component: 'Input'
|
||||
},
|
||||
// TODO UPLOAD
|
||||
{
|
||||
label: '应用图标',
|
||||
field: 'logo',
|
||||
required: true,
|
||||
component: 'Input'
|
||||
component: 'FileUpload',
|
||||
componentProps: {
|
||||
fileType: 'image',
|
||||
maxCount: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '应用描述',
|
||||
|
|
Loading…
Reference in New Issue