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 | 78 | }, |
| 79 | 79 | #elseif($column.htmlType == "imageUpload")## 图片上传 |
| 80 | 80 | form: { |
| 81 | - component: 'UploadImg', | |
| 82 | - componentProps: { | |
| 83 | - limit: 1 | |
| 84 | - } | |
| 81 | + component: 'UploadImg' // 单图上传,多图为UploadImgs | |
| 85 | 82 | }, |
| 86 | 83 | #elseif($column.htmlType == "fileUpload")## 图片上传 |
| 87 | 84 | form: { | ... | ... |
yudao-ui-admin-vue3/src/components/Form/src/componentMap.ts
| ... | ... | @@ -21,7 +21,7 @@ import { |
| 21 | 21 | } from 'element-plus' |
| 22 | 22 | import { InputPassword } from '@/components/InputPassword' |
| 23 | 23 | import { Editor } from '@/components/Editor' |
| 24 | -import { UploadImg, UploadFile } from '@/components/UploadFile' | |
| 24 | +import { UploadImg, UploadImgs, UploadFile } from '@/components/UploadFile' | |
| 25 | 25 | import { ComponentName } from '@/types/components' |
| 26 | 26 | |
| 27 | 27 | const componentMap: Recordable<Component, ComponentName> = { |
| ... | ... | @@ -48,6 +48,7 @@ const componentMap: Recordable<Component, ComponentName> = { |
| 48 | 48 | InputPassword: InputPassword, |
| 49 | 49 | Editor: Editor, |
| 50 | 50 | UploadImg: UploadImg, |
| 51 | + UploadImgs: UploadImgs, | |
| 51 | 52 | UploadFile: UploadFile |
| 52 | 53 | } |
| 53 | 54 | ... | ... |
yudao-ui-admin-vue3/src/components/UploadFile/index.ts
yudao-ui-admin-vue3/src/components/UploadFile/src/UploadFile.vue
| ... | ... | @@ -32,8 +32,8 @@ |
| 32 | 32 | </el-upload> |
| 33 | 33 | </div> |
| 34 | 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 | 37 | import { useMessage } from '@/hooks/web/useMessage' |
| 38 | 38 | import { propTypes } from '@/utils/propTypes' |
| 39 | 39 | import { getAccessToken, getTenantId } from '@/utils/auth' |
| ... | ... | @@ -43,7 +43,10 @@ const message = useMessage() // 消息弹窗 |
| 43 | 43 | const emit = defineEmits(['update:modelValue']) |
| 44 | 44 | |
| 45 | 45 | const props = defineProps({ |
| 46 | - modelValue: propTypes.oneOfType([String, Object, Array]), | |
| 46 | + modelValue: { | |
| 47 | + type: Array as PropType<UploadUserFile[]>, | |
| 48 | + required: true | |
| 49 | + }, | |
| 47 | 50 | title: propTypes.string.def('文件上传'), |
| 48 | 51 | updateUrl: propTypes.string.def(import.meta.env.VITE_UPLOAD_URL), |
| 49 | 52 | fileType: propTypes.array.def(['doc', 'xls', 'ppt', 'txt', 'pdf']), // 文件类型, 例如['png', 'jpg', 'jpeg'] |
| ... | ... | @@ -57,40 +60,12 @@ const props = defineProps({ |
| 57 | 60 | const valueRef = ref(props.modelValue) |
| 58 | 61 | const uploadRef = ref<UploadInstance>() |
| 59 | 62 | const uploadList = ref<UploadUserFile[]>([]) |
| 60 | -const fileList = ref<UploadUserFile[]>([]) | |
| 63 | +const fileList = ref<UploadUserFile[]>(props.modelValue) | |
| 61 | 64 | const uploadNumber = ref<number>(0) |
| 62 | 65 | const uploadHeaders = ref({ |
| 63 | 66 | Authorization: 'Bearer ' + getAccessToken(), |
| 64 | 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 | 70 | const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => { |
| 96 | 71 | if (fileList.value.length >= props.limit) { | ... | ... |
yudao-ui-admin-vue3/src/components/UploadFile/src/UploadImg.vue
| 1 | 1 | <template> |
| 2 | - <div class="component-upload-image"> | |
| 2 | + <div class="upload-box"> | |
| 3 | 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 | 4 | :action="updateUrl" |
| 5 | + :id="uuid" | |
| 6 | + :class="['upload', drag ? 'no-border' : '']" | |
| 7 | + :multiple="false" | |
| 8 | + :show-file-list="false" | |
| 12 | 9 | :headers="uploadHeaders" |
| 13 | - :limit="props.limit" | |
| 14 | 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 | 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 | 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 | 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 | 58 | import { useMessage } from '@/hooks/web/useMessage' |
| 59 | +import { generateUUID } from '@/utils' | |
| 34 | 60 | import { propTypes } from '@/utils/propTypes' |
| 35 | 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 | 76 | const props = defineProps({ |
| 42 | - modelValue: propTypes.oneOfType([String, Object, Array]), | |
| 43 | - title: propTypes.string.def('图片上传'), | |
| 77 | + modelValue: propTypes.string.def(''), | |
| 44 | 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 | 100 | const uploadHeaders = ref({ |
| 59 | 101 | Authorization: 'Bearer ' + getAccessToken(), |
| 60 | 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 | 142 | </script> |
| 162 | 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 | 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
yudao-ui-admin-vue3/src/utils/index.ts
| ... | ... | @@ -69,7 +69,7 @@ export const trim = (str: string) => { |
| 69 | 69 | * @param {Date | number | string} time 需要转换的时间 |
| 70 | 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 | 73 | if (!time) return '' |
| 74 | 74 | else { |
| 75 | 75 | const date = new Date(time) |
| ... | ... | @@ -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 | 104 | const str: string = 'xxxxx-xxxxx-4xxxx-yxxxx-xxxxx'.replace(/[xy]/g, (c: string) => { |
| 105 | 105 | const r: number = (Math.random() * 16) | 0 |
| 106 | 106 | const v: number = c === 'x' ? r : (r & 0x3) | 0x8 |
| ... | ... | @@ -108,3 +108,34 @@ export function toAnyString() { |
| 108 | 108 | }) |
| 109 | 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