feat: file upload

pull/12/head
xingyu 2023-05-10 15:45:48 +08:00
parent e63d8dbcb9
commit 611da354a1
5 changed files with 468 additions and 2 deletions

View File

@ -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)

View File

@ -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
}
// uploadchange
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') {
//returnUrltrue
if (props.returnUrl) {
handlePathChange()
} else {
//returnUrlfalse
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>

View File

@ -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>

View File

@ -116,3 +116,4 @@ export type ComponentType =
| 'Divider'
| 'ApiTransfer'
| 'Editor'
| 'FileUpload'

View File

@ -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: '应用描述',