Commit 9de323e09ca121bc24ff5fb21771d766f107036c
1 parent
2d791301
perf: 完善上传组件
Showing
9 changed files
with
560 additions
and
189 deletions
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/data.ts.vm
| @@ -78,10 +78,7 @@ const crudSchemas = reactive<VxeCrudSchema>({ | @@ -78,10 +78,7 @@ const crudSchemas = reactive<VxeCrudSchema>({ | ||
| 78 | }, | 78 | }, |
| 79 | #elseif($column.htmlType == "imageUpload")## 图片上传 | 79 | #elseif($column.htmlType == "imageUpload")## 图片上传 |
| 80 | form: { | 80 | form: { |
| 81 | - component: 'UploadImg', | ||
| 82 | - componentProps: { | ||
| 83 | - limit: 1 | ||
| 84 | - } | 81 | + component: 'UploadImg' // 单图上传,多图为UploadImgs |
| 85 | }, | 82 | }, |
| 86 | #elseif($column.htmlType == "fileUpload")## 图片上传 | 83 | #elseif($column.htmlType == "fileUpload")## 图片上传 |
| 87 | form: { | 84 | form: { |
yudao-ui-admin-vue3/src/components/Form/src/componentMap.ts
| @@ -21,7 +21,7 @@ import { | @@ -21,7 +21,7 @@ import { | ||
| 21 | } from 'element-plus' | 21 | } from 'element-plus' |
| 22 | import { InputPassword } from '@/components/InputPassword' | 22 | import { InputPassword } from '@/components/InputPassword' |
| 23 | import { Editor } from '@/components/Editor' | 23 | import { Editor } from '@/components/Editor' |
| 24 | -import { UploadImg, UploadFile } from '@/components/UploadFile' | 24 | +import { UploadImg, UploadImgs, UploadFile } from '@/components/UploadFile' |
| 25 | import { ComponentName } from '@/types/components' | 25 | import { ComponentName } from '@/types/components' |
| 26 | 26 | ||
| 27 | const componentMap: Recordable<Component, ComponentName> = { | 27 | const componentMap: Recordable<Component, ComponentName> = { |
| @@ -48,6 +48,7 @@ const componentMap: Recordable<Component, ComponentName> = { | @@ -48,6 +48,7 @@ const componentMap: Recordable<Component, ComponentName> = { | ||
| 48 | InputPassword: InputPassword, | 48 | InputPassword: InputPassword, |
| 49 | Editor: Editor, | 49 | Editor: Editor, |
| 50 | UploadImg: UploadImg, | 50 | UploadImg: UploadImg, |
| 51 | + UploadImgs: UploadImgs, | ||
| 51 | UploadFile: UploadFile | 52 | UploadFile: UploadFile |
| 52 | } | 53 | } |
| 53 | 54 |
yudao-ui-admin-vue3/src/components/UploadFile/index.ts
| 1 | import UploadImg from './src/UploadImg.vue' | 1 | import UploadImg from './src/UploadImg.vue' |
| 2 | +import UploadImgs from './src/UploadImgs.vue' | ||
| 2 | import UploadFile from './src/UploadFile.vue' | 3 | import UploadFile from './src/UploadFile.vue' |
| 3 | 4 | ||
| 4 | -export { UploadImg, UploadFile } | 5 | +export { UploadImg, UploadImgs, UploadFile } |
yudao-ui-admin-vue3/src/components/UploadFile/src/UploadFile.vue
| @@ -32,8 +32,8 @@ | @@ -32,8 +32,8 @@ | ||
| 32 | </el-upload> | 32 | </el-upload> |
| 33 | </div> | 33 | </div> |
| 34 | </template> | 34 | </template> |
| 35 | -<script setup lang="ts"> | ||
| 36 | -import { ref, watch } from 'vue' | 35 | +<script setup lang="ts" name="UploadFile"> |
| 36 | +import { PropType, ref } from 'vue' | ||
| 37 | import { useMessage } from '@/hooks/web/useMessage' | 37 | import { useMessage } from '@/hooks/web/useMessage' |
| 38 | import { propTypes } from '@/utils/propTypes' | 38 | import { propTypes } from '@/utils/propTypes' |
| 39 | import { getAccessToken, getTenantId } from '@/utils/auth' | 39 | import { getAccessToken, getTenantId } from '@/utils/auth' |
| @@ -43,7 +43,10 @@ const message = useMessage() // 消息弹窗 | @@ -43,7 +43,10 @@ const message = useMessage() // 消息弹窗 | ||
| 43 | const emit = defineEmits(['update:modelValue']) | 43 | const emit = defineEmits(['update:modelValue']) |
| 44 | 44 | ||
| 45 | const props = defineProps({ | 45 | const props = defineProps({ |
| 46 | - modelValue: propTypes.oneOfType([String, Object, Array]), | 46 | + modelValue: { |
| 47 | + type: Array as PropType<UploadUserFile[]>, | ||
| 48 | + required: true | ||
| 49 | + }, | ||
| 47 | title: propTypes.string.def('文件上传'), | 50 | title: propTypes.string.def('文件上传'), |
| 48 | updateUrl: propTypes.string.def(import.meta.env.VITE_UPLOAD_URL), | 51 | updateUrl: propTypes.string.def(import.meta.env.VITE_UPLOAD_URL), |
| 49 | fileType: propTypes.array.def(['doc', 'xls', 'ppt', 'txt', 'pdf']), // 文件类型, 例如['png', 'jpg', 'jpeg'] | 52 | fileType: propTypes.array.def(['doc', 'xls', 'ppt', 'txt', 'pdf']), // 文件类型, 例如['png', 'jpg', 'jpeg'] |
| @@ -57,40 +60,12 @@ const props = defineProps({ | @@ -57,40 +60,12 @@ const props = defineProps({ | ||
| 57 | const valueRef = ref(props.modelValue) | 60 | const valueRef = ref(props.modelValue) |
| 58 | const uploadRef = ref<UploadInstance>() | 61 | const uploadRef = ref<UploadInstance>() |
| 59 | const uploadList = ref<UploadUserFile[]>([]) | 62 | const uploadList = ref<UploadUserFile[]>([]) |
| 60 | -const fileList = ref<UploadUserFile[]>([]) | 63 | +const fileList = ref<UploadUserFile[]>(props.modelValue) |
| 61 | const uploadNumber = ref<number>(0) | 64 | const uploadNumber = ref<number>(0) |
| 62 | const uploadHeaders = ref({ | 65 | const uploadHeaders = ref({ |
| 63 | Authorization: 'Bearer ' + getAccessToken(), | 66 | Authorization: 'Bearer ' + getAccessToken(), |
| 64 | 'tenant-id': getTenantId() | 67 | 'tenant-id': getTenantId() |
| 65 | }) | 68 | }) |
| 66 | -watch( | ||
| 67 | - () => props.modelValue, | ||
| 68 | - (val) => { | ||
| 69 | - if (val) { | ||
| 70 | - // 首先将值转为数组, 当只穿了一个图片时,会报map方法错误 | ||
| 71 | - const list = Array.isArray(props.modelValue) | ||
| 72 | - ? props.modelValue | ||
| 73 | - : Array.isArray(props.modelValue?.split(',')) | ||
| 74 | - ? props.modelValue?.split(',') | ||
| 75 | - : Array.of(props.modelValue) | ||
| 76 | - // 然后将数组转为对象数组 | ||
| 77 | - fileList.value = list.map((item) => { | ||
| 78 | - if (typeof item === 'string') { | ||
| 79 | - // edit by 芋道源码 | ||
| 80 | - item = { name: item, url: item } | ||
| 81 | - } | ||
| 82 | - return item | ||
| 83 | - }) | ||
| 84 | - } else { | ||
| 85 | - fileList.value = [] | ||
| 86 | - return [] | ||
| 87 | - } | ||
| 88 | - }, | ||
| 89 | - { | ||
| 90 | - deep: true, | ||
| 91 | - immediate: true | ||
| 92 | - } | ||
| 93 | -) | ||
| 94 | // 文件上传之前判断 | 69 | // 文件上传之前判断 |
| 95 | const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => { | 70 | const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => { |
| 96 | if (fileList.value.length >= props.limit) { | 71 | if (fileList.value.length >= props.limit) { |
yudao-ui-admin-vue3/src/components/UploadFile/src/UploadImg.vue
| 1 | <template> | 1 | <template> |
| 2 | - <div class="component-upload-image"> | 2 | + <div class="upload-box"> |
| 3 | <el-upload | 3 | <el-upload |
| 4 | - ref="uploadRef" | ||
| 5 | - :multiple="props.limit > 1" | ||
| 6 | - name="file" | ||
| 7 | - v-model="valueRef" | ||
| 8 | - list-type="picture-card" | ||
| 9 | - v-model:file-list="fileList" | ||
| 10 | - :show-file-list="true" | ||
| 11 | :action="updateUrl" | 4 | :action="updateUrl" |
| 5 | + :id="uuid" | ||
| 6 | + :class="['upload', drag ? 'no-border' : '']" | ||
| 7 | + :multiple="false" | ||
| 8 | + :show-file-list="false" | ||
| 12 | :headers="uploadHeaders" | 9 | :headers="uploadHeaders" |
| 13 | - :limit="props.limit" | ||
| 14 | :before-upload="beforeUpload" | 10 | :before-upload="beforeUpload" |
| 15 | - :on-exceed="handleExceed" | ||
| 16 | - :on-success="handleFileSuccess" | ||
| 17 | - :on-error="excelUploadError" | ||
| 18 | - :on-remove="handleRemove" | ||
| 19 | - :on-preview="handlePictureCardPreview" | ||
| 20 | - :class="{ hide: fileList.length >= props.limit }" | 11 | + :on-success="uploadSuccess" |
| 12 | + :on-error="uploadError" | ||
| 13 | + :drag="drag" | ||
| 14 | + :accept="fileType.join(',')" | ||
| 21 | > | 15 | > |
| 22 | - <Icon icon="ep:upload-filled" /> | 16 | + <template v-if="modelValue"> |
| 17 | + <img :src="modelValue" class="upload-image" /> | ||
| 18 | + <div class="upload-handle" @click.stop> | ||
| 19 | + <div class="handle-icon" @click="editImg"> | ||
| 20 | + <Icon icon="ep:edit" /> | ||
| 21 | + <span>{{ t('action.edit') }}</span> | ||
| 22 | + </div> | ||
| 23 | + <div class="handle-icon" @click="imgViewVisible = true"> | ||
| 24 | + <Icon icon="ep:zoom-in" /> | ||
| 25 | + <span>{{ t('action.detail') }}</span> | ||
| 26 | + </div> | ||
| 27 | + <div class="handle-icon" @click="deleteImg"> | ||
| 28 | + <Icon icon="ep:delete" /> | ||
| 29 | + <span>{{ t('action.del') }}</span> | ||
| 30 | + </div> | ||
| 31 | + </div> | ||
| 32 | + </template> | ||
| 33 | + <template v-else> | ||
| 34 | + <div class="upload-empty"> | ||
| 35 | + <slot name="empty"> | ||
| 36 | + <Icon icon="ep:plus" /> | ||
| 37 | + <!-- <span>请上传图片</span> --> | ||
| 38 | + </slot> | ||
| 39 | + </div> | ||
| 40 | + </template> | ||
| 23 | </el-upload> | 41 | </el-upload> |
| 42 | + <div class="el-upload__tip"> | ||
| 43 | + <slot name="tip"></slot> | ||
| 44 | + </div> | ||
| 45 | + <el-image-viewer | ||
| 46 | + v-if="imgViewVisible" | ||
| 47 | + @close="imgViewVisible = false" | ||
| 48 | + :url-list="[modelValue]" | ||
| 49 | + /> | ||
| 24 | </div> | 50 | </div> |
| 25 | - <!-- 文件列表 --> | ||
| 26 | - <Dialog v-model="dialogVisible" title="预览" width="800" append-to-body> | ||
| 27 | - <img :src="dialogImageUrl" style="display: block; max-width: 100%; margin: 0 auto" /> | ||
| 28 | - </Dialog> | ||
| 29 | </template> | 51 | </template> |
| 30 | -<script setup lang="ts"> | ||
| 31 | -import { ref, watch } from 'vue' | ||
| 32 | -import { Dialog } from '@/components/Dialog' | 52 | + |
| 53 | +<script setup lang="ts" name="UploadImg"> | ||
| 54 | +import { ref } from 'vue' | ||
| 55 | +import type { UploadProps } from 'element-plus' | ||
| 56 | +import { ElUpload, ElNotification, ElImageViewer } from 'element-plus' | ||
| 57 | +import { useI18n } from '@/hooks/web/useI18n' | ||
| 33 | import { useMessage } from '@/hooks/web/useMessage' | 58 | import { useMessage } from '@/hooks/web/useMessage' |
| 59 | +import { generateUUID } from '@/utils' | ||
| 34 | import { propTypes } from '@/utils/propTypes' | 60 | import { propTypes } from '@/utils/propTypes' |
| 35 | import { getAccessToken, getTenantId } from '@/utils/auth' | 61 | import { getAccessToken, getTenantId } from '@/utils/auth' |
| 36 | -import { ElUpload, UploadInstance, UploadProps, UploadRawFile, UploadUserFile } from 'element-plus' | ||
| 37 | 62 | ||
| 38 | -const message = useMessage() // 消息弹窗 | ||
| 39 | -const emit = defineEmits(['update:modelValue']) | 63 | +type FileTypes = |
| 64 | + | 'image/apng' | ||
| 65 | + | 'image/bmp' | ||
| 66 | + | 'image/gif' | ||
| 67 | + | 'image/jpeg' | ||
| 68 | + | 'image/pjpeg' | ||
| 69 | + | 'image/png' | ||
| 70 | + | 'image/svg+xml' | ||
| 71 | + | 'image/tiff' | ||
| 72 | + | 'image/webp' | ||
| 73 | + | 'image/x-icon' | ||
| 40 | 74 | ||
| 75 | +// 接受父组件参数 | ||
| 41 | const props = defineProps({ | 76 | const props = defineProps({ |
| 42 | - modelValue: propTypes.oneOfType([String, Object, Array]), | ||
| 43 | - title: propTypes.string.def('图片上传'), | 77 | + modelValue: propTypes.string.def(''), |
| 44 | updateUrl: propTypes.string.def(import.meta.env.VITE_UPLOAD_URL), | 78 | updateUrl: propTypes.string.def(import.meta.env.VITE_UPLOAD_URL), |
| 45 | - fileType: propTypes.array.def(['jpg', 'png', 'gif', 'jpeg']), // 文件类型, 例如['png', 'jpg', 'jpeg'] | ||
| 46 | - fileSize: propTypes.number.def(5), // 大小限制(MB) | ||
| 47 | - limit: propTypes.number.def(1), // 数量限制 | ||
| 48 | - isShowTip: propTypes.bool.def(false) // 是否显示提示 | 79 | + drag: propTypes.bool.def(true), // 是否支持拖拽上传 ==> 非必传(默认为 true) |
| 80 | + disabled: propTypes.bool.def(false), // 是否禁用上传组件 ==> 非必传(默认为 false) | ||
| 81 | + fileSize: propTypes.number.def(5), // 图片大小限制 ==> 非必传(默认为 5M) | ||
| 82 | + fileType: propTypes.array.def(['image/jpeg', 'image/png', 'image/gif']), // 图片类型限制 ==> 非必传(默认为 ["image/jpeg", "image/png", "image/gif"]) | ||
| 83 | + height: propTypes.string.def('150px'), // 组件高度 ==> 非必传(默认为 150px) | ||
| 84 | + width: propTypes.string.def('150px'), // 组件宽度 ==> 非必传(默认为 150px) | ||
| 85 | + borderRadius: propTypes.string.def('8px') // 组件边框圆角 ==> 非必传(默认为 8px) | ||
| 49 | }) | 86 | }) |
| 50 | -// ========== 上传相关 ========== | ||
| 51 | -const valueRef = ref(props.modelValue) | ||
| 52 | -const uploadRef = ref<UploadInstance>() | ||
| 53 | -const uploadList = ref<UploadUserFile[]>([]) | ||
| 54 | -const fileList = ref<UploadUserFile[]>([]) | ||
| 55 | -const uploadNumber = ref<number>(0) | ||
| 56 | -const dialogImageUrl = ref() | ||
| 57 | -const dialogVisible = ref(false) | 87 | +const { t } = useI18n() // 国际化 |
| 88 | +const message = useMessage() // 消息弹窗 | ||
| 89 | +// 生成组件唯一id | ||
| 90 | +const uuid = ref('id-' + generateUUID()) | ||
| 91 | +// 查看图片 | ||
| 92 | +const imgViewVisible = ref(false) | ||
| 93 | + | ||
| 94 | +const emit = defineEmits(['update:modelValue']) | ||
| 95 | + | ||
| 96 | +const deleteImg = () => { | ||
| 97 | + emit('update:modelValue', '') | ||
| 98 | +} | ||
| 99 | + | ||
| 58 | const uploadHeaders = ref({ | 100 | const uploadHeaders = ref({ |
| 59 | Authorization: 'Bearer ' + getAccessToken(), | 101 | Authorization: 'Bearer ' + getAccessToken(), |
| 60 | 'tenant-id': getTenantId() | 102 | 'tenant-id': getTenantId() |
| 61 | }) | 103 | }) |
| 62 | -watch( | ||
| 63 | - () => props.modelValue, | ||
| 64 | - (val) => { | ||
| 65 | - if (val) { | ||
| 66 | - // 首先将值转为数组, 当只穿了一个图片时,会报map方法错误 | ||
| 67 | - const list = Array.isArray(props.modelValue) | ||
| 68 | - ? props.modelValue | ||
| 69 | - : Array.isArray(props.modelValue?.split(',')) | ||
| 70 | - ? props.modelValue?.split(',') | ||
| 71 | - : Array.of(props.modelValue) | ||
| 72 | - // 然后将数组转为对象数组 | ||
| 73 | - fileList.value = list.map((item) => { | ||
| 74 | - if (typeof item === 'string') { | ||
| 75 | - // edit by 芋道源码 | ||
| 76 | - item = { name: item, url: item } | ||
| 77 | - } | ||
| 78 | - return item | ||
| 79 | - }) | ||
| 80 | - } else { | ||
| 81 | - fileList.value = [] | ||
| 82 | - return [] | ||
| 83 | - } | ||
| 84 | - }, | ||
| 85 | - { | ||
| 86 | - deep: true, | ||
| 87 | - immediate: true | ||
| 88 | - } | ||
| 89 | -) | ||
| 90 | -// 文件上传之前判断 | ||
| 91 | -const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => { | ||
| 92 | - if (fileList.value.length >= props.limit) { | ||
| 93 | - message.error(`上传文件数量不能超过${props.limit}个!`) | ||
| 94 | - return false | ||
| 95 | - } | ||
| 96 | - let fileExtension = '' | ||
| 97 | - if (file.name.lastIndexOf('.') > -1) { | ||
| 98 | - fileExtension = file.name.slice(file.name.lastIndexOf('.') + 1) | ||
| 99 | - } | ||
| 100 | - const isImg = props.fileType.some((type: string) => { | ||
| 101 | - if (file.type.indexOf(type) > -1) return true | ||
| 102 | - return !!(fileExtension && fileExtension.indexOf(type) > -1) | ||
| 103 | - }) | ||
| 104 | - const isLimit = file.size < props.fileSize * 1024 * 1024 | ||
| 105 | - if (!isImg) { | ||
| 106 | - message.error(`文件格式不正确, 请上传${props.fileType.join('/')}格式!`) | ||
| 107 | - return false | ||
| 108 | - } | ||
| 109 | - if (!isLimit) { | ||
| 110 | - message.error(`上传文件大小不能超过${props.fileSize}MB!`) | ||
| 111 | - return false | ||
| 112 | - } | ||
| 113 | - message.success('正在上传文件,请稍候...') | ||
| 114 | - uploadNumber.value++ | ||
| 115 | -} | ||
| 116 | -// 处理上传的文件发生变化 | ||
| 117 | -// const handleFileChange = (uploadFile: UploadFile): void => { | ||
| 118 | -// uploadRef.value.data.path = uploadFile.name | ||
| 119 | -// } | ||
| 120 | -// 文件上传成功 | ||
| 121 | -const handleFileSuccess: UploadProps['onSuccess'] = (res: any): void => { | ||
| 122 | - message.success('上传成功') | ||
| 123 | - uploadList.value.push({ name: res.data, url: res.data }) | ||
| 124 | - if (uploadList.value.length == uploadNumber.value) { | ||
| 125 | - fileList.value = fileList.value.concat(uploadList.value) | ||
| 126 | - uploadList.value = [] | ||
| 127 | - uploadNumber.value = 0 | ||
| 128 | - emit('update:modelValue', listToString(fileList.value)) | ||
| 129 | - } | ||
| 130 | -} | ||
| 131 | -// 文件数超出提示 | ||
| 132 | -const handleExceed: UploadProps['onExceed'] = (): void => { | ||
| 133 | - message.error(`上传文件数量不能超过${props.limit}个!`) | ||
| 134 | -} | ||
| 135 | -// 上传错误提示 | ||
| 136 | -const excelUploadError: UploadProps['onError'] = (): void => { | ||
| 137 | - message.error('导入数据失败,请您重新上传!') | 104 | + |
| 105 | +const editImg = () => { | ||
| 106 | + const dom = document.querySelector(`#${uuid.value} .el-upload__input`) | ||
| 107 | + dom && dom.dispatchEvent(new MouseEvent('click')) | ||
| 138 | } | 108 | } |
| 139 | -// 删除上传文件 | ||
| 140 | -const handleRemove = (file) => { | ||
| 141 | - const findex = fileList.value.map((f) => f.name).indexOf(file.name) | ||
| 142 | - if (findex > -1) { | ||
| 143 | - fileList.value.splice(findex, 1) | ||
| 144 | - emit('update:modelValue', listToString(fileList.value)) | ||
| 145 | - } | 109 | + |
| 110 | +const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => { | ||
| 111 | + const imgSize = rawFile.size / 1024 / 1024 < props.fileSize | ||
| 112 | + const imgType = props.fileType | ||
| 113 | + if (!imgType.includes(rawFile.type as FileTypes)) | ||
| 114 | + ElNotification({ | ||
| 115 | + title: '温馨提示', | ||
| 116 | + message: '上传图片不符合所需的格式!', | ||
| 117 | + type: 'warning' | ||
| 118 | + }) | ||
| 119 | + if (!imgSize) | ||
| 120 | + ElNotification({ | ||
| 121 | + title: '温馨提示', | ||
| 122 | + message: `上传图片大小不能超过 ${props.fileSize}M!`, | ||
| 123 | + type: 'warning' | ||
| 124 | + }) | ||
| 125 | + return imgType.includes(rawFile.type as FileTypes) && imgSize | ||
| 146 | } | 126 | } |
| 147 | -// 对象转成指定字符串分隔 | ||
| 148 | -const listToString = (list: UploadUserFile[], separator?: string) => { | ||
| 149 | - let strs = '' | ||
| 150 | - separator = separator || ',' | ||
| 151 | - for (let i in list) { | ||
| 152 | - strs += list[i].url + separator | ||
| 153 | - } | ||
| 154 | - return strs != '' ? strs.substr(0, strs.length - 1) : '' | 127 | + |
| 128 | +// 图片上传成功提示 | ||
| 129 | +const uploadSuccess: UploadProps['onSuccess'] = (res: any): void => { | ||
| 130 | + message.success('上传成功') | ||
| 131 | + emit('update:modelValue', res.data) | ||
| 155 | } | 132 | } |
| 156 | -// 预览 | ||
| 157 | -const handlePictureCardPreview: UploadProps['onPreview'] = (file) => { | ||
| 158 | - dialogImageUrl.value = file.url | ||
| 159 | - dialogVisible.value = true | 133 | + |
| 134 | +// 图片上传错误提示 | ||
| 135 | +const uploadError = () => { | ||
| 136 | + ElNotification({ | ||
| 137 | + title: '温馨提示', | ||
| 138 | + message: '图片上传失败,请您重新上传!', | ||
| 139 | + type: 'error' | ||
| 140 | + }) | ||
| 160 | } | 141 | } |
| 161 | </script> | 142 | </script> |
| 162 | <style scoped lang="scss"> | 143 | <style scoped lang="scss"> |
| 163 | -// .el-upload--picture-card 控制加号部分 | ||
| 164 | -:deep(.hide .el-upload--picture-card) { | ||
| 165 | - display: none; | 144 | +.is-error { |
| 145 | + .upload { | ||
| 146 | + :deep(.el-upload), | ||
| 147 | + :deep(.el-upload-dragger) { | ||
| 148 | + border: 1px dashed var(--el-color-danger) !important; | ||
| 149 | + &:hover { | ||
| 150 | + border-color: var(--el-color-primary) !important; | ||
| 151 | + } | ||
| 152 | + } | ||
| 153 | + } | ||
| 166 | } | 154 | } |
| 167 | -// 去掉动画效果 | ||
| 168 | -:deep(.el-list-enter-active, .el-list-leave-active) { | ||
| 169 | - transition: all 0s; | 155 | +:deep(.disabled) { |
| 156 | + .el-upload, | ||
| 157 | + .el-upload-dragger { | ||
| 158 | + cursor: not-allowed !important; | ||
| 159 | + background: var(--el-disabled-bg-color); | ||
| 160 | + border: 1px dashed var(--el-border-color-darker) !important; | ||
| 161 | + &:hover { | ||
| 162 | + border: 1px dashed var(--el-border-color-darker) !important; | ||
| 163 | + } | ||
| 164 | + } | ||
| 170 | } | 165 | } |
| 171 | - | ||
| 172 | -:deep(.el-list-enter, .el-list-leave-active) { | ||
| 173 | - opacity: 0; | ||
| 174 | - transform: translateY(0); | 166 | +.upload-box { |
| 167 | + .no-border { | ||
| 168 | + :deep(.el-upload) { | ||
| 169 | + border: none !important; | ||
| 170 | + } | ||
| 171 | + } | ||
| 172 | + :deep(.upload) { | ||
| 173 | + .el-upload { | ||
| 174 | + position: relative; | ||
| 175 | + display: flex; | ||
| 176 | + align-items: center; | ||
| 177 | + justify-content: center; | ||
| 178 | + width: v-bind(width); | ||
| 179 | + height: v-bind(height); | ||
| 180 | + overflow: hidden; | ||
| 181 | + border: 1px dashed var(--el-border-color-darker); | ||
| 182 | + border-radius: v-bind(borderRadius); | ||
| 183 | + transition: var(--el-transition-duration-fast); | ||
| 184 | + &:hover { | ||
| 185 | + border-color: var(--el-color-primary); | ||
| 186 | + .upload-handle { | ||
| 187 | + opacity: 1; | ||
| 188 | + } | ||
| 189 | + } | ||
| 190 | + .el-upload-dragger { | ||
| 191 | + display: flex; | ||
| 192 | + align-items: center; | ||
| 193 | + justify-content: center; | ||
| 194 | + width: 100%; | ||
| 195 | + height: 100%; | ||
| 196 | + padding: 0; | ||
| 197 | + overflow: hidden; | ||
| 198 | + background-color: transparent; | ||
| 199 | + border: 1px dashed var(--el-border-color-darker); | ||
| 200 | + border-radius: v-bind(borderRadius); | ||
| 201 | + &:hover { | ||
| 202 | + border: 1px dashed var(--el-color-primary); | ||
| 203 | + } | ||
| 204 | + } | ||
| 205 | + .el-upload-dragger.is-dragover { | ||
| 206 | + background-color: var(--el-color-primary-light-9); | ||
| 207 | + border: 2px dashed var(--el-color-primary) !important; | ||
| 208 | + } | ||
| 209 | + .upload-image { | ||
| 210 | + width: 100%; | ||
| 211 | + height: 100%; | ||
| 212 | + object-fit: contain; | ||
| 213 | + } | ||
| 214 | + .upload-empty { | ||
| 215 | + position: relative; | ||
| 216 | + display: flex; | ||
| 217 | + flex-direction: column; | ||
| 218 | + align-items: center; | ||
| 219 | + justify-content: center; | ||
| 220 | + font-size: 12px; | ||
| 221 | + line-height: 30px; | ||
| 222 | + color: var(--el-color-info); | ||
| 223 | + .el-icon { | ||
| 224 | + font-size: 28px; | ||
| 225 | + color: var(--el-text-color-secondary); | ||
| 226 | + } | ||
| 227 | + } | ||
| 228 | + .upload-handle { | ||
| 229 | + position: absolute; | ||
| 230 | + top: 0; | ||
| 231 | + right: 0; | ||
| 232 | + box-sizing: border-box; | ||
| 233 | + display: flex; | ||
| 234 | + align-items: center; | ||
| 235 | + justify-content: center; | ||
| 236 | + width: 100%; | ||
| 237 | + height: 100%; | ||
| 238 | + cursor: pointer; | ||
| 239 | + background: rgb(0 0 0 / 60%); | ||
| 240 | + opacity: 0; | ||
| 241 | + transition: var(--el-transition-duration-fast); | ||
| 242 | + .handle-icon { | ||
| 243 | + display: flex; | ||
| 244 | + flex-direction: column; | ||
| 245 | + align-items: center; | ||
| 246 | + justify-content: center; | ||
| 247 | + padding: 0 6%; | ||
| 248 | + color: aliceblue; | ||
| 249 | + .el-icon { | ||
| 250 | + margin-bottom: 40%; | ||
| 251 | + font-size: 130%; | ||
| 252 | + line-height: 130%; | ||
| 253 | + } | ||
| 254 | + span { | ||
| 255 | + font-size: 85%; | ||
| 256 | + line-height: 85%; | ||
| 257 | + } | ||
| 258 | + } | ||
| 259 | + } | ||
| 260 | + } | ||
| 261 | + } | ||
| 262 | + .el-upload__tip { | ||
| 263 | + line-height: 18px; | ||
| 264 | + text-align: center; | ||
| 265 | + } | ||
| 175 | } | 266 | } |
| 176 | </style> | 267 | </style> |
yudao-ui-admin-vue3/src/components/UploadFile/src/UploadImgs.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="upload-box"> | ||
| 3 | + <el-upload | ||
| 4 | + :action="updateUrl" | ||
| 5 | + list-type="picture-card" | ||
| 6 | + :class="['upload', drag ? 'no-border' : '']" | ||
| 7 | + v-model:file-list="fileList" | ||
| 8 | + :multiple="true" | ||
| 9 | + :limit="limit" | ||
| 10 | + :headers="uploadHeaders" | ||
| 11 | + :before-upload="beforeUpload" | ||
| 12 | + :on-exceed="handleExceed" | ||
| 13 | + :on-success="uploadSuccess" | ||
| 14 | + :on-error="uploadError" | ||
| 15 | + :drag="drag" | ||
| 16 | + :accept="fileType.join(',')" | ||
| 17 | + > | ||
| 18 | + <div class="upload-empty"> | ||
| 19 | + <slot name="empty"> | ||
| 20 | + <Icon icon="ep:plus" /> | ||
| 21 | + <!-- <span>请上传图片</span> --> | ||
| 22 | + </slot> | ||
| 23 | + </div> | ||
| 24 | + <template #file="{ file }"> | ||
| 25 | + <img :src="file.url" class="upload-image" /> | ||
| 26 | + <div class="upload-handle" @click.stop> | ||
| 27 | + <div class="handle-icon" @click="handlePictureCardPreview(file)"> | ||
| 28 | + <Icon icon="ep:zoom-in" /> | ||
| 29 | + <span>查看</span> | ||
| 30 | + </div> | ||
| 31 | + <div class="handle-icon" @click="handleRemove(file)"> | ||
| 32 | + <Icon icon="ep:delete" /> | ||
| 33 | + <span>删除</span> | ||
| 34 | + </div> | ||
| 35 | + </div> | ||
| 36 | + </template> | ||
| 37 | + </el-upload> | ||
| 38 | + <div class="el-upload__tip"> | ||
| 39 | + <slot name="tip"></slot> | ||
| 40 | + </div> | ||
| 41 | + <el-image-viewer | ||
| 42 | + v-if="imgViewVisible" | ||
| 43 | + @close="imgViewVisible = false" | ||
| 44 | + :url-list="[viewImageUrl]" | ||
| 45 | + /> | ||
| 46 | + </div> | ||
| 47 | +</template> | ||
| 48 | +<script setup lang="ts" name="UploadImgs"> | ||
| 49 | +import { PropType, ref } from 'vue' | ||
| 50 | +import { ElUpload, ElNotification, ElImageViewer } from 'element-plus' | ||
| 51 | +import type { UploadProps, UploadFile, UploadUserFile } from 'element-plus' | ||
| 52 | +import { useMessage } from '@/hooks/web/useMessage' | ||
| 53 | +import { propTypes } from '@/utils/propTypes' | ||
| 54 | +import { getAccessToken, getTenantId } from '@/utils/auth' | ||
| 55 | + | ||
| 56 | +const message = useMessage() // 消息弹窗 | ||
| 57 | + | ||
| 58 | +type FileTypes = | ||
| 59 | + | 'image/apng' | ||
| 60 | + | 'image/bmp' | ||
| 61 | + | 'image/gif' | ||
| 62 | + | 'image/jpeg' | ||
| 63 | + | 'image/pjpeg' | ||
| 64 | + | 'image/png' | ||
| 65 | + | 'image/svg+xml' | ||
| 66 | + | 'image/tiff' | ||
| 67 | + | 'image/webp' | ||
| 68 | + | 'image/x-icon' | ||
| 69 | + | ||
| 70 | +const props = defineProps({ | ||
| 71 | + modelValue: { | ||
| 72 | + type: Array as PropType<UploadUserFile[]>, | ||
| 73 | + required: true | ||
| 74 | + }, | ||
| 75 | + updateUrl: propTypes.string.def(import.meta.env.VITE_UPLOAD_URL), | ||
| 76 | + drag: propTypes.bool.def(true), // 是否支持拖拽上传 ==> 非必传(默认为 true) | ||
| 77 | + disabled: propTypes.bool.def(false), // 是否禁用上传组件 ==> 非必传(默认为 false) | ||
| 78 | + limit: propTypes.number.def(5), // 最大图片上传数 ==> 非必传(默认为 5张) | ||
| 79 | + fileSize: propTypes.number.def(5), // 图片大小限制 ==> 非必传(默认为 5M) | ||
| 80 | + fileType: propTypes.array.def(['image/jpeg', 'image/png', 'image/gif']), // 图片类型限制 ==> 非必传(默认为 ["image/jpeg", "image/png", "image/gif"]) | ||
| 81 | + height: propTypes.string.def('150px'), // 组件高度 ==> 非必传(默认为 150px) | ||
| 82 | + width: propTypes.string.def('150px'), // 组件宽度 ==> 非必传(默认为 150px) | ||
| 83 | + borderRadius: propTypes.string.def('8px') // 组件边框圆角 ==> 非必传(默认为 8px) | ||
| 84 | +}) | ||
| 85 | + | ||
| 86 | +const uploadHeaders = ref({ | ||
| 87 | + Authorization: 'Bearer ' + getAccessToken(), | ||
| 88 | + 'tenant-id': getTenantId() | ||
| 89 | +}) | ||
| 90 | + | ||
| 91 | +const fileList = ref<UploadUserFile[]>(props.modelValue) | ||
| 92 | + | ||
| 93 | +/** | ||
| 94 | + * @description 文件上传之前判断 | ||
| 95 | + * @param rawFile 上传的文件 | ||
| 96 | + * */ | ||
| 97 | +const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => { | ||
| 98 | + const imgSize = rawFile.size / 1024 / 1024 < props.fileSize | ||
| 99 | + const imgType = props.fileType | ||
| 100 | + if (!imgType.includes(rawFile.type as FileTypes)) | ||
| 101 | + ElNotification({ | ||
| 102 | + title: '温馨提示', | ||
| 103 | + message: '上传图片不符合所需的格式!', | ||
| 104 | + type: 'warning' | ||
| 105 | + }) | ||
| 106 | + if (!imgSize) | ||
| 107 | + ElNotification({ | ||
| 108 | + title: '温馨提示', | ||
| 109 | + message: `上传图片大小不能超过 ${props.fileSize}M!`, | ||
| 110 | + type: 'warning' | ||
| 111 | + }) | ||
| 112 | + return imgType.includes(rawFile.type as FileTypes) && imgSize | ||
| 113 | +} | ||
| 114 | + | ||
| 115 | +// 图片上传成功 | ||
| 116 | +interface UploadEmits { | ||
| 117 | + (e: 'update:modelValue', value: UploadUserFile[]): void | ||
| 118 | +} | ||
| 119 | +const emit = defineEmits<UploadEmits>() | ||
| 120 | +const uploadSuccess = (response, uploadFile: UploadFile) => { | ||
| 121 | + if (!response) return | ||
| 122 | + uploadFile.url = response.data | ||
| 123 | + emit('update:modelValue', fileList.value) | ||
| 124 | + message.success('上传成功') | ||
| 125 | +} | ||
| 126 | + | ||
| 127 | +// 删除图片 | ||
| 128 | +const handleRemove = (uploadFile: UploadFile) => { | ||
| 129 | + fileList.value = fileList.value.filter( | ||
| 130 | + (item) => item.url !== uploadFile.url || item.name !== uploadFile.name | ||
| 131 | + ) | ||
| 132 | + emit('update:modelValue', fileList.value) | ||
| 133 | +} | ||
| 134 | + | ||
| 135 | +// 图片上传错误提示 | ||
| 136 | +const uploadError = () => { | ||
| 137 | + ElNotification({ | ||
| 138 | + title: '温馨提示', | ||
| 139 | + message: '图片上传失败,请您重新上传!', | ||
| 140 | + type: 'error' | ||
| 141 | + }) | ||
| 142 | +} | ||
| 143 | + | ||
| 144 | +// 文件数超出提示 | ||
| 145 | +const handleExceed = () => { | ||
| 146 | + ElNotification({ | ||
| 147 | + title: '温馨提示', | ||
| 148 | + message: `当前最多只能上传 ${props.limit} 张图片,请移除后上传!`, | ||
| 149 | + type: 'warning' | ||
| 150 | + }) | ||
| 151 | +} | ||
| 152 | + | ||
| 153 | +// 图片预览 | ||
| 154 | +const viewImageUrl = ref('') | ||
| 155 | +const imgViewVisible = ref(false) | ||
| 156 | +const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => { | ||
| 157 | + viewImageUrl.value = uploadFile.url! | ||
| 158 | + imgViewVisible.value = true | ||
| 159 | +} | ||
| 160 | +</script> | ||
| 161 | + | ||
| 162 | +<style scoped lang="scss"> | ||
| 163 | +.is-error { | ||
| 164 | + .upload { | ||
| 165 | + :deep(.el-upload--picture-card), | ||
| 166 | + :deep(.el-upload-dragger) { | ||
| 167 | + border: 1px dashed var(--el-color-danger) !important; | ||
| 168 | + &:hover { | ||
| 169 | + border-color: var(--el-color-primary) !important; | ||
| 170 | + } | ||
| 171 | + } | ||
| 172 | + } | ||
| 173 | +} | ||
| 174 | +:deep(.disabled) { | ||
| 175 | + .el-upload--picture-card, | ||
| 176 | + .el-upload-dragger { | ||
| 177 | + cursor: not-allowed; | ||
| 178 | + background: var(--el-disabled-bg-color) !important; | ||
| 179 | + border: 1px dashed var(--el-border-color-darker); | ||
| 180 | + &:hover { | ||
| 181 | + border-color: var(--el-border-color-darker) !important; | ||
| 182 | + } | ||
| 183 | + } | ||
| 184 | +} | ||
| 185 | +.upload-box { | ||
| 186 | + .no-border { | ||
| 187 | + :deep(.el-upload--picture-card) { | ||
| 188 | + border: none !important; | ||
| 189 | + } | ||
| 190 | + } | ||
| 191 | + :deep(.upload) { | ||
| 192 | + .el-upload-dragger { | ||
| 193 | + display: flex; | ||
| 194 | + align-items: center; | ||
| 195 | + justify-content: center; | ||
| 196 | + width: 100%; | ||
| 197 | + height: 100%; | ||
| 198 | + padding: 0; | ||
| 199 | + overflow: hidden; | ||
| 200 | + border: 1px dashed var(--el-border-color-darker); | ||
| 201 | + border-radius: v-bind(borderRadius); | ||
| 202 | + &:hover { | ||
| 203 | + border: 1px dashed var(--el-color-primary); | ||
| 204 | + } | ||
| 205 | + } | ||
| 206 | + .el-upload-dragger.is-dragover { | ||
| 207 | + background-color: var(--el-color-primary-light-9); | ||
| 208 | + border: 2px dashed var(--el-color-primary) !important; | ||
| 209 | + } | ||
| 210 | + .el-upload-list__item, | ||
| 211 | + .el-upload--picture-card { | ||
| 212 | + width: v-bind(width); | ||
| 213 | + height: v-bind(height); | ||
| 214 | + background-color: transparent; | ||
| 215 | + border-radius: v-bind(borderRadius); | ||
| 216 | + } | ||
| 217 | + .upload-image { | ||
| 218 | + width: 100%; | ||
| 219 | + height: 100%; | ||
| 220 | + object-fit: contain; | ||
| 221 | + } | ||
| 222 | + .upload-handle { | ||
| 223 | + position: absolute; | ||
| 224 | + top: 0; | ||
| 225 | + right: 0; | ||
| 226 | + box-sizing: border-box; | ||
| 227 | + display: flex; | ||
| 228 | + align-items: center; | ||
| 229 | + justify-content: center; | ||
| 230 | + width: 100%; | ||
| 231 | + height: 100%; | ||
| 232 | + cursor: pointer; | ||
| 233 | + background: rgb(0 0 0 / 60%); | ||
| 234 | + opacity: 0; | ||
| 235 | + transition: var(--el-transition-duration-fast); | ||
| 236 | + .handle-icon { | ||
| 237 | + display: flex; | ||
| 238 | + flex-direction: column; | ||
| 239 | + align-items: center; | ||
| 240 | + justify-content: center; | ||
| 241 | + padding: 0 6%; | ||
| 242 | + color: aliceblue; | ||
| 243 | + .el-icon { | ||
| 244 | + margin-bottom: 15%; | ||
| 245 | + font-size: 140%; | ||
| 246 | + } | ||
| 247 | + span { | ||
| 248 | + font-size: 100%; | ||
| 249 | + } | ||
| 250 | + } | ||
| 251 | + } | ||
| 252 | + .el-upload-list__item { | ||
| 253 | + &:hover { | ||
| 254 | + .upload-handle { | ||
| 255 | + opacity: 1; | ||
| 256 | + } | ||
| 257 | + } | ||
| 258 | + } | ||
| 259 | + .upload-empty { | ||
| 260 | + display: flex; | ||
| 261 | + flex-direction: column; | ||
| 262 | + align-items: center; | ||
| 263 | + font-size: 12px; | ||
| 264 | + line-height: 30px; | ||
| 265 | + color: var(--el-color-info); | ||
| 266 | + .el-icon { | ||
| 267 | + font-size: 28px; | ||
| 268 | + color: var(--el-text-color-secondary); | ||
| 269 | + } | ||
| 270 | + } | ||
| 271 | + } | ||
| 272 | + .el-upload__tip { | ||
| 273 | + line-height: 15px; | ||
| 274 | + text-align: center; | ||
| 275 | + } | ||
| 276 | +} | ||
| 277 | +</style> |
yudao-ui-admin-vue3/src/types/components.d.ts
| @@ -22,6 +22,7 @@ export type ComponentName = | @@ -22,6 +22,7 @@ export type ComponentName = | ||
| 22 | | 'InputPassword' | 22 | | 'InputPassword' |
| 23 | | 'Editor' | 23 | | 'Editor' |
| 24 | | 'UploadImg' | 24 | | 'UploadImg' |
| 25 | + | 'UploadImgs' | ||
| 25 | | 'UploadFile' | 26 | | 'UploadFile' |
| 26 | 27 | ||
| 27 | export type ColProps = { | 28 | export type ColProps = { |
yudao-ui-admin-vue3/src/utils/index.ts
| @@ -69,7 +69,7 @@ export const trim = (str: string) => { | @@ -69,7 +69,7 @@ export const trim = (str: string) => { | ||
| 69 | * @param {Date | number | string} time 需要转换的时间 | 69 | * @param {Date | number | string} time 需要转换的时间 |
| 70 | * @param {String} fmt 需要转换的格式 如 yyyy-MM-dd、yyyy-MM-dd HH:mm:ss | 70 | * @param {String} fmt 需要转换的格式 如 yyyy-MM-dd、yyyy-MM-dd HH:mm:ss |
| 71 | */ | 71 | */ |
| 72 | -export function formatTime(time: Date | number | string, fmt: string) { | 72 | +export const formatTime = (time: Date | number | string, fmt: string) => { |
| 73 | if (!time) return '' | 73 | if (!time) return '' |
| 74 | else { | 74 | else { |
| 75 | const date = new Date(time) | 75 | const date = new Date(time) |
| @@ -100,7 +100,7 @@ export function formatTime(time: Date | number | string, fmt: string) { | @@ -100,7 +100,7 @@ export function formatTime(time: Date | number | string, fmt: string) { | ||
| 100 | /** | 100 | /** |
| 101 | * 生成随机字符串 | 101 | * 生成随机字符串 |
| 102 | */ | 102 | */ |
| 103 | -export function toAnyString() { | 103 | +export const toAnyString = () => { |
| 104 | const str: string = 'xxxxx-xxxxx-4xxxx-yxxxx-xxxxx'.replace(/[xy]/g, (c: string) => { | 104 | const str: string = 'xxxxx-xxxxx-4xxxx-yxxxx-xxxxx'.replace(/[xy]/g, (c: string) => { |
| 105 | const r: number = (Math.random() * 16) | 0 | 105 | const r: number = (Math.random() * 16) | 0 |
| 106 | const v: number = c === 'x' ? r : (r & 0x3) | 0x8 | 106 | const v: number = c === 'x' ? r : (r & 0x3) | 0x8 |
| @@ -108,3 +108,34 @@ export function toAnyString() { | @@ -108,3 +108,34 @@ export function toAnyString() { | ||
| 108 | }) | 108 | }) |
| 109 | return str | 109 | return str |
| 110 | } | 110 | } |
| 111 | + | ||
| 112 | +export const generateUUID = () => { | ||
| 113 | + if (typeof crypto === 'object') { | ||
| 114 | + if (typeof crypto.randomUUID === 'function') { | ||
| 115 | + return crypto.randomUUID() | ||
| 116 | + } | ||
| 117 | + if (typeof crypto.getRandomValues === 'function' && typeof Uint8Array === 'function') { | ||
| 118 | + const callback = (c: any) => { | ||
| 119 | + const num = Number(c) | ||
| 120 | + return (num ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (num / 4)))).toString( | ||
| 121 | + 16 | ||
| 122 | + ) | ||
| 123 | + } | ||
| 124 | + return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, callback) | ||
| 125 | + } | ||
| 126 | + } | ||
| 127 | + let timestamp = new Date().getTime() | ||
| 128 | + let performanceNow = | ||
| 129 | + (typeof performance !== 'undefined' && performance.now && performance.now() * 1000) || 0 | ||
| 130 | + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { | ||
| 131 | + let random = Math.random() * 16 | ||
| 132 | + if (timestamp > 0) { | ||
| 133 | + random = (timestamp + random) % 16 | 0 | ||
| 134 | + timestamp = Math.floor(timestamp / 16) | ||
| 135 | + } else { | ||
| 136 | + random = (performanceNow + random) % 16 | 0 | ||
| 137 | + performanceNow = Math.floor(performanceNow / 16) | ||
| 138 | + } | ||
| 139 | + return (c === 'x' ? random : (random & 0x3) | 0x8).toString(16) | ||
| 140 | + }) | ||
| 141 | +} |
yudao-ui-admin-vue3/src/views/system/oauth2/client/client.data.ts
| @@ -43,10 +43,7 @@ const crudSchemas = reactive<VxeCrudSchema>({ | @@ -43,10 +43,7 @@ const crudSchemas = reactive<VxeCrudSchema>({ | ||
| 43 | } | 43 | } |
| 44 | }, | 44 | }, |
| 45 | form: { | 45 | form: { |
| 46 | - component: 'UploadImg', | ||
| 47 | - componentProps: { | ||
| 48 | - limit: 1 | ||
| 49 | - } | 46 | + component: 'UploadImg' |
| 50 | } | 47 | } |
| 51 | }, | 48 | }, |
| 52 | { | 49 | { |