diff --git a/src/components/Form/index.ts b/src/components/Form/index.ts index eb7b797..480eade 100644 --- a/src/components/Form/index.ts +++ b/src/components/Form/index.ts @@ -13,5 +13,6 @@ export { default as ApiTree } from './src/components/ApiTree.vue' export { default as ApiRadioGroup } from './src/components/ApiRadioGroup.vue' export { default as ApiCascader } from './src/components/ApiCascader.vue' export { default as ApiTransfer } from './src/components/ApiTransfer.vue' +export { default as ImageUpload } from './src/components/ImageUpload.vue' export { BasicForm } diff --git a/src/components/Form/src/componentMap.ts b/src/components/Form/src/componentMap.ts index 0ef3d28..c7c71ac 100644 --- a/src/components/Form/src/componentMap.ts +++ b/src/components/Form/src/componentMap.ts @@ -20,7 +20,6 @@ import type { ComponentType } from './types' /** * Component list, register here to setting it in the form */ - import ApiRadioGroup from './components/ApiRadioGroup.vue' import RadioButtonGroup from './components/RadioButtonGroup.vue' import ApiSelect from './components/ApiSelect.vue' @@ -29,6 +28,7 @@ import ApiTreeSelect from './components/ApiTreeSelect.vue' import ApiCascader from './components/ApiCascader.vue' import ApiTransfer from './components/ApiTransfer.vue' import FileUpload from './components/FileUpload.vue' +import ImageUpload from './components/ImageUpload.vue' import { BasicUpload } from '@/components/Upload' import { StrengthMeter } from '@/components/StrengthMeter' import { IconPicker } from '@/components/Icon' @@ -44,7 +44,7 @@ componentMap.set('InputSearch', Input.Search) componentMap.set('InputTextArea', Input.TextArea) componentMap.set('InputNumber', InputNumber) componentMap.set('AutoComplete', AutoComplete) - +componentMap.set('ImageUpload', ImageUpload) componentMap.set('Select', Select) componentMap.set('ApiSelect', ApiSelect) componentMap.set('ApiTree', ApiTree) @@ -67,6 +67,7 @@ componentMap.set('MonthPicker', DatePicker.MonthPicker) componentMap.set('RangePicker', DatePicker.RangePicker) componentMap.set('WeekPicker', DatePicker.WeekPicker) componentMap.set('TimePicker', TimePicker) +componentMap.set('TimeRangePicker', TimePicker.TimeRangePicker) componentMap.set('StrengthMeter', StrengthMeter) componentMap.set('IconPicker', IconPicker) componentMap.set('InputCountDown', CountdownInput) diff --git a/src/components/Form/src/components/FileUpload.vue b/src/components/Form/src/components/FileUpload.vue index 43ec3f7..1b5c6e5 100644 --- a/src/components/Form/src/components/FileUpload.vue +++ b/src/components/Form/src/components/FileUpload.vue @@ -57,19 +57,15 @@ const isMaxCount = computed(() => props.maxCount > 0 && fileList.value.length >= 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/*' diff --git a/src/components/Form/src/components/ImageUpload.vue b/src/components/Form/src/components/ImageUpload.vue new file mode 100644 index 0000000..0a57d41 --- /dev/null +++ b/src/components/Form/src/components/ImageUpload.vue @@ -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> diff --git a/src/components/Form/src/types/index.ts b/src/components/Form/src/types/index.ts index dbf21c8..5bc05cf 100644 --- a/src/components/Form/src/types/index.ts +++ b/src/components/Form/src/types/index.ts @@ -106,9 +106,11 @@ export type ComponentType = | 'RangePicker' | 'WeekPicker' | 'TimePicker' + | 'TimeRangePicker' | 'Switch' | 'StrengthMeter' | 'Upload' + | 'ImageUpload' | 'IconPicker' | 'Render' | 'Slider'