5 changed files with 229 additions and 6 deletions
@ -0,0 +1,223 @@
|
||||
<script lang="ts" setup> |
||||
import type { PropType } from 'vue' |
||||
import { reactive, ref, watch } from 'vue' |
||||
import type { UploadProps } from 'ant-design-vue' |
||||
import { Modal, Upload, message } from 'ant-design-vue' |
||||
import type { UploadFile } from 'ant-design-vue/lib/upload/interface' |
||||
import { join } from 'lodash-es' |
||||
import { PlusOutlined } from '@ant-design/icons-vue' |
||||
import { useI18n } from '@/hooks/web/useI18n' |
||||
import { buildShortUUID } from '@/utils/uuid' |
||||
import { isArray, isNotEmpty, isUrl } from '@/utils/is' |
||||
import { useRuleFormItem } from '@/hooks/component/useFormItem' |
||||
import { useAttrs } from '@/hooks/core/useAttrs' |
||||
|
||||
type ImageUploadType = 'text' | 'picture' | 'picture-card' |
||||
|
||||
defineOptions({ name: 'ImageUpload', inheritAttrs: false }) |
||||
|
||||
const props = defineProps({ |
||||
value: [Array, String], |
||||
api: { |
||||
type: Function as PropType<(file: UploadFile) => Promise<string>>, |
||||
default: null, |
||||
}, |
||||
listType: { |
||||
type: String as PropType<ImageUploadType>, |
||||
default: () => 'picture-card', |
||||
}, |
||||
// 文件类型 |
||||
fileType: { |
||||
type: Array, |
||||
default: () => ['image/png', 'image/jpeg'], |
||||
}, |
||||
multiple: { |
||||
type: Boolean, |
||||
default: () => false, |
||||
}, |
||||
// 最大数量的文件 |
||||
maxCount: { |
||||
type: Number, |
||||
default: () => 1, |
||||
}, |
||||
// 最小数量的文件 |
||||
minCount: { |
||||
type: Number, |
||||
default: () => 0, |
||||
}, |
||||
// 文件最大多少MB |
||||
maxSize: { |
||||
type: Number, |
||||
default: () => 2, |
||||
}, |
||||
}) |
||||
|
||||
const emit = defineEmits(['change', 'update:value']) |
||||
|
||||
const attrs = useAttrs() |
||||
const { t } = useI18n() |
||||
const previewOpen = ref(false) |
||||
const previewImage = ref('') |
||||
const emitData = ref<any[] | any | undefined>() |
||||
const fileList = ref<UploadFile[]>([]) |
||||
// Embedded in the form, just use the hook binding to perform form verification |
||||
const [state] = useRuleFormItem(props, 'value', 'change', emitData) |
||||
const fileState = reactive<{ |
||||
newList: any[] |
||||
newStr: string |
||||
oldStr: string |
||||
}>({ |
||||
newList: [], |
||||
newStr: '', |
||||
oldStr: '', |
||||
}) |
||||
watch( |
||||
() => fileList.value, |
||||
(v) => { |
||||
fileState.newList = v |
||||
.filter((item: any) => { |
||||
return item?.url && item.status === 'done' && isUrl(item?.url) |
||||
}) |
||||
.map((item: any) => item?.url) |
||||
fileState.newStr = join(fileState.newList) |
||||
// 不相等代表数据变更 |
||||
if (fileState.newStr !== fileState.oldStr) { |
||||
fileState.oldStr = fileState.newStr |
||||
emitData.value = props.multiple ? fileState.newList : fileState.newStr |
||||
state.value = props.multiple ? fileState.newList : fileState.newStr |
||||
} |
||||
}, |
||||
{ |
||||
deep: true, |
||||
}, |
||||
) |
||||
watch( |
||||
() => state.value, |
||||
(v) => { |
||||
changeFileValue(v) |
||||
emit('update:value', v) |
||||
}, |
||||
) |
||||
function changeFileValue(value: any) { |
||||
const stateStr = props.multiple ? join((value as string[]) || []) : value || '' |
||||
if (stateStr !== fileState.oldStr) { |
||||
fileState.oldStr = stateStr |
||||
let list: string[] = [] |
||||
if (props.multiple) { |
||||
if (isNotEmpty(value)) { |
||||
if (isArray(value)) |
||||
list = value as string[] |
||||
else |
||||
list.push(value as string) |
||||
} |
||||
} |
||||
else { |
||||
if (isNotEmpty(value)) |
||||
list.push(value as string) |
||||
} |
||||
fileList.value = list.map((item) => { |
||||
const uuid = buildShortUUID() |
||||
return { |
||||
uid: uuid, |
||||
name: uuid, |
||||
status: 'done', |
||||
url: item, |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
/** 关闭查看 */ |
||||
function handleCancel() { |
||||
previewOpen.value = false |
||||
} |
||||
/** 查看图片 */ |
||||
async function handlePreview(file: UploadProps['fileList'][number]) { |
||||
if (!file.url && !file.preview) |
||||
file.preview = (await getBase64(file.originFileObj)) as string |
||||
|
||||
previewImage.value = file.url || file.preview |
||||
previewOpen.value = true |
||||
} |
||||
/** 上传前校验 */ |
||||
const handleBeforeUpload: UploadProps['beforeUpload'] = (file) => { |
||||
if (fileList.value.length > props.maxCount) { |
||||
fileList.value.splice(props.maxCount, fileList.value.length - props.maxCount) |
||||
message.error(t('component.upload.maxNumber', [props.maxCount])) |
||||
return Upload.LIST_IGNORE |
||||
} |
||||
const isPNG = props.fileType.includes(file.type) |
||||
if (!isPNG) |
||||
message.error(t('component.upload.acceptUpload', [props.fileType.toString()])) |
||||
|
||||
const isLt2M = file.size / 1024 / 1024 < props.maxSize |
||||
if (!isLt2M) |
||||
message.error(t('component.upload.maxSizeMultiple', [props.maxSize])) |
||||
|
||||
if (!(isPNG && isLt2M)) |
||||
fileList.value.pop() |
||||
|
||||
return (isPNG && isLt2M) || Upload.LIST_IGNORE |
||||
} |
||||
/** 自定义上传 */ |
||||
async function handleCustomRequest(option: any) { |
||||
const { file } = option |
||||
await props |
||||
.api(option) |
||||
.then((url) => { |
||||
file.url = url |
||||
file.status = 'done' |
||||
fileList.value.pop() |
||||
fileList.value.push(file) |
||||
}) |
||||
.catch(() => { |
||||
fileList.value.pop() |
||||
}) |
||||
} |
||||
function getBase64(file: File) { |
||||
return new Promise((resolve, reject) => { |
||||
const reader = new FileReader() |
||||
reader.readAsDataURL(file) |
||||
reader.onload = () => resolve(reader.result) |
||||
reader.onerror = error => reject(error) |
||||
}) |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="clearfix"> |
||||
<Upload |
||||
v-model:file-list="fileList" |
||||
v-bind="attrs" |
||||
v-model:value="state" |
||||
:list-type="listType" |
||||
:multiple="multiple" |
||||
:max-count="maxCount" |
||||
:custom-request="handleCustomRequest" |
||||
:before-upload="handleBeforeUpload" |
||||
@preview="handlePreview" |
||||
> |
||||
<div v-if="fileList.length < maxCount"> |
||||
<PlusOutlined /> |
||||
<div style="margin-top: 8px"> |
||||
{{ t('component.upload.upload') }} |
||||
</div> |
||||
</div> |
||||
</Upload> |
||||
<Modal :open="previewOpen" :footer="null" @cancel="handleCancel"> |
||||
<img alt="example" style="width: 100%" :src="previewImage"> |
||||
</Modal> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped> |
||||
/* you can make up upload button and sample style by using stylesheets */ |
||||
.ant-upload-select-picture-card i { |
||||
font-size: 32px; |
||||
color: #999; |
||||
} |
||||
|
||||
.ant-upload-select-picture-card .ant-upload-text { |
||||
margin-top: 8px; |
||||
color: #666; |
||||
} |
||||
</style> |
Reference in new issue