Browse Source
* fix(vxe-table): theme dark is not work * perf(ImageUpload): 优化Upload组件目录结构,以及图片上传组件pull/3244/head
committed by
GitHub
16 changed files with 213 additions and 281 deletions
@ -1,253 +0,0 @@ |
|||
<template> |
|||
<div class="clearfix"> |
|||
<a-upload |
|||
v-model:file-list="fileList" |
|||
:list-type="listType" |
|||
:multiple="multiple" |
|||
:max-count="maxCount" |
|||
:customRequest="handleCustomRequest" |
|||
:before-upload="handleBeforeUpload" |
|||
v-bind="$attrs" |
|||
@preview="handlePreview" |
|||
v-model:value="state" |
|||
> |
|||
<div v-if="fileList.length < maxCount"> |
|||
<plus-outlined /> |
|||
<div style="margin-top: 8px"> |
|||
{{ t('component.upload.upload') }} |
|||
</div> |
|||
</div> |
|||
</a-upload> |
|||
<a-modal :open="previewOpen" :footer="null" @cancel="handleCancel"> |
|||
<img alt="example" style="width: 100%" :src="previewImage" /> |
|||
</a-modal> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { defineComponent, PropType, reactive, ref, watch } from 'vue'; |
|||
import { message, Modal, Upload, UploadProps } from 'ant-design-vue'; |
|||
import { UploadFile } from 'ant-design-vue/lib/upload/interface'; |
|||
import { useI18n } from '@/hooks/web/useI18n'; |
|||
import { join } from 'lodash-es'; |
|||
import { buildShortUUID } from '@/utils/uuid'; |
|||
import { isArray, isNotEmpty, isUrl } from '@/utils/is'; |
|||
import { useRuleFormItem } from '@/hooks/component/useFormItem'; |
|||
import { useAttrs } from '@vben/hooks'; |
|||
import { PlusOutlined } from '@ant-design/icons-vue'; |
|||
|
|||
type ImageUploadType = 'text' | 'picture' | 'picture-card'; |
|||
|
|||
export default defineComponent({ |
|||
name: 'ImageUpload', |
|||
components: { |
|||
PlusOutlined, |
|||
AUpload: Upload, |
|||
AModal: Modal, |
|||
}, |
|||
inheritAttrs: false, |
|||
props: { |
|||
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, |
|||
}, |
|||
}, |
|||
emits: ['change', 'update:value'], |
|||
setup(props, { emit }) { |
|||
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, |
|||
}; |
|||
}); |
|||
} |
|||
} |
|||
|
|||
/** 关闭查看 */ |
|||
const handleCancel = () => { |
|||
previewOpen.value = false; |
|||
}; |
|||
|
|||
/** 查看图片 */ |
|||
// @ts-ignore |
|||
const handlePreview = async (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; |
|||
}; |
|||
|
|||
/** 自定义上传 */ |
|||
const handleCustomRequest = async (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); |
|||
}); |
|||
} |
|||
|
|||
return { |
|||
previewOpen, |
|||
fileList, |
|||
state, |
|||
attrs, |
|||
t, |
|||
handlePreview, |
|||
handleBeforeUpload, |
|||
handleCustomRequest, |
|||
handleCancel, |
|||
previewImage, |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
|
|||
<style scoped> |
|||
/* you can make up upload button and sample style by using stylesheets */ |
|||
.ant-upload-select-picture-card i { |
|||
color: #999; |
|||
font-size: 32px; |
|||
} |
|||
|
|||
.ant-upload-select-picture-card .ant-upload-text { |
|||
margin-top: 8px; |
|||
color: #666; |
|||
} |
|||
</style> |
|||
@ -1,4 +1,6 @@ |
|||
import { withInstall } from '/@/utils'; |
|||
import basicUpload from './src/BasicUpload.vue'; |
|||
import uploadImage from './src/components/ImageUpload.vue'; |
|||
|
|||
export const ImageUpload = withInstall(uploadImage); |
|||
export const BasicUpload = withInstall(basicUpload); |
|||
|
|||
@ -1,5 +1,5 @@ |
|||
<script lang="tsx"> |
|||
import { fileListProps } from './props'; |
|||
import { fileListProps } from '../props'; |
|||
import { isFunction, isDef } from '/@/utils/is'; |
|||
import { useSortable } from '/@/hooks/web/useSortable'; |
|||
import { useModalContext } from '/@/components/Modal/src/hooks/useModalContext'; |
|||
@ -0,0 +1,161 @@ |
|||
<template> |
|||
<div> |
|||
<Upload |
|||
v-bind="$attrs" |
|||
v-model:file-list="fileList" |
|||
:list-type="listType" |
|||
:accept="getStringAccept" |
|||
:before-upload="beforeUpload" |
|||
:custom-request="customRequest" |
|||
@preview="handlePreview" |
|||
@remove="handleRemove" |
|||
> |
|||
<div v-if="fileList && fileList.length < maxNumber"> |
|||
<plus-outlined /> |
|||
<div style="margin-top: 8px">{{ t('component.upload.upload') }}</div> |
|||
</div> |
|||
</Upload> |
|||
<Modal :open="previewOpen" :title="previewTitle" :footer="null" @cancel="handleCancel"> |
|||
<img alt="" style="width: 100%" :src="previewImage" /> |
|||
</Modal> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts" setup name="ImageUpload"> |
|||
import { ref, toRefs, watch } from 'vue'; |
|||
import { PlusOutlined } from '@ant-design/icons-vue'; |
|||
import { Upload, Modal } from 'ant-design-vue'; |
|||
import type { UploadProps } from 'ant-design-vue'; |
|||
import { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface'; |
|||
import { useMessage } from '@/hooks/web/useMessage'; |
|||
import { isArray, isFunction } from '@/utils/is'; |
|||
import { warn } from '@/utils/log'; |
|||
import { useI18n } from '@/hooks/web/useI18n'; |
|||
import { useUploadType } from '../hooks/useUpload'; |
|||
import { uploadContainerProps } from '../props'; |
|||
import { isImgTypeByName } from '../helper'; |
|||
|
|||
const emit = defineEmits(['change', 'update:value', 'delete']); |
|||
const props = defineProps({ |
|||
...uploadContainerProps, |
|||
}); |
|||
const { t } = useI18n(); |
|||
const { createMessage } = useMessage(); |
|||
const { accept, helpText, maxNumber, maxSize } = toRefs(props); |
|||
const { getStringAccept } = useUploadType({ |
|||
acceptRef: accept, |
|||
helpTextRef: helpText, |
|||
maxNumberRef: maxNumber, |
|||
maxSizeRef: maxSize, |
|||
}); |
|||
const previewOpen = ref<boolean>(false); |
|||
const previewImage = ref<string>(''); |
|||
const previewTitle = ref<string>(''); |
|||
|
|||
const fileList = ref<UploadProps['fileList']>([]); |
|||
const isLtMsg = ref<boolean>(true); |
|||
const isActMsg = ref<boolean>(true); |
|||
|
|||
watch( |
|||
() => props.value, |
|||
(v) => { |
|||
if (isArray(v)) { |
|||
fileList.value = v.map((url, i) => ({ |
|||
uid: String(-i), |
|||
name: url ? url.substring(url.lastIndexOf('/') + 1) : 'image.png', |
|||
status: 'done', |
|||
url, |
|||
})); |
|||
} |
|||
}, |
|||
{ |
|||
immediate: true, |
|||
deep: true, |
|||
}, |
|||
); |
|||
|
|||
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); |
|||
}); |
|||
} |
|||
|
|||
const handlePreview = async (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; |
|||
previewTitle.value = file.name || file.url.substring(file.url.lastIndexOf('/') + 1); |
|||
}; |
|||
|
|||
const handleRemove = async (file: UploadProps['fileList'][number]) => { |
|||
if (fileList.value) { |
|||
const index = fileList.value.findIndex((item: any) => item.uuid === file.uuid); |
|||
index !== -1 && fileList.value.splice(index, 1); |
|||
emit('change', fileList.value); |
|||
emit('delete', file); |
|||
} |
|||
}; |
|||
|
|||
const handleCancel = () => { |
|||
previewOpen.value = false; |
|||
previewTitle.value = ''; |
|||
}; |
|||
|
|||
const beforeUpload = (file: File) => { |
|||
const { maxSize, accept } = props; |
|||
const { name } = file; |
|||
isActMsg.value = isImgTypeByName(name); |
|||
if (!isActMsg.value) { |
|||
createMessage.error(t('component.upload.acceptUpload', [accept])); |
|||
isActMsg.value = false; |
|||
// 防止弹出多个错误提示 |
|||
setTimeout(() => (isActMsg.value = true), 1000); |
|||
} |
|||
isLtMsg.value = file.size / 1024 / 1024 > maxSize; |
|||
if (isLtMsg.value) { |
|||
createMessage.error(t('component.upload.maxSizeMultiple', [maxSize])); |
|||
isLtMsg.value = false; |
|||
// 防止弹出多个错误提示 |
|||
setTimeout(() => (isLtMsg.value = true), 1000); |
|||
} |
|||
return (isActMsg.value && !isLtMsg.value) || Upload.LIST_IGNORE; |
|||
}; |
|||
|
|||
async function customRequest(info: UploadRequestOption<any>) { |
|||
const { api } = props; |
|||
if (!api || !isFunction(api)) { |
|||
return warn('upload api must exist and be a function'); |
|||
} |
|||
try { |
|||
const res = await props.api?.({ |
|||
data: { |
|||
...(props.uploadParams || {}), |
|||
}, |
|||
file: info.file, |
|||
name: props.name, |
|||
filename: props.filename, |
|||
}); |
|||
info.onSuccess!(res.data); |
|||
emit('change', fileList.value); |
|||
} catch (e: any) { |
|||
info.onError!(e); |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="less"> |
|||
.ant-upload-select-picture-card i { |
|||
color: #999; |
|||
font-size: 32px; |
|||
} |
|||
|
|||
.ant-upload-select-picture-card .ant-upload-text { |
|||
margin-top: 8px; |
|||
color: #666; |
|||
} |
|||
</style> |
|||
@ -1,6 +1,6 @@ |
|||
import type { BasicColumn, ActionItem } from '/@/components/Table'; |
|||
import { FileBasicColumn, FileItem, PreviewFileItem, UploadResultStatus } from './typing'; |
|||
import { isImgTypeByName } from './helper'; |
|||
import { FileBasicColumn, FileItem, PreviewFileItem, UploadResultStatus } from '../types/typing'; |
|||
import { isImgTypeByName } from '../helper'; |
|||
import { Progress, Tag } from 'ant-design-vue'; |
|||
import TableAction from '/@/components/Table/src/components/TableAction.vue'; |
|||
import ThumbUrl from './ThumbUrl.vue'; |
|||
Loading…
Reference in new issue