Procházet zdrojové kódy

perf: 完善上传组件

xingyu před 2 roky
rodič
revize
9de323e09c

+ 1 - 4
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/data.ts.vm

@@ -78,10 +78,7 @@ const crudSchemas = reactive<VxeCrudSchema>({
       },
       #elseif($column.htmlType == "imageUpload")## 图片上传
       form: {
-        component: 'UploadImg',
-        componentProps: {
-          limit: 1
-        }
+        component: 'UploadImg' // 单图上传,多图为UploadImgs
       },
       #elseif($column.htmlType == "fileUpload")## 图片上传
       form: {

+ 2 - 1
yudao-ui-admin-vue3/src/components/Form/src/componentMap.ts

@@ -21,7 +21,7 @@ import {
 } from 'element-plus'
 import { InputPassword } from '@/components/InputPassword'
 import { Editor } from '@/components/Editor'
-import { UploadImg, UploadFile } from '@/components/UploadFile'
+import { UploadImg, UploadImgs, UploadFile } from '@/components/UploadFile'
 import { ComponentName } from '@/types/components'
 
 const componentMap: Recordable<Component, ComponentName> = {
@@ -48,6 +48,7 @@ const componentMap: Recordable<Component, ComponentName> = {
   InputPassword: InputPassword,
   Editor: Editor,
   UploadImg: UploadImg,
+  UploadImgs: UploadImgs,
   UploadFile: UploadFile
 }
 

+ 2 - 1
yudao-ui-admin-vue3/src/components/UploadFile/index.ts

@@ -1,4 +1,5 @@
 import UploadImg from './src/UploadImg.vue'
+import UploadImgs from './src/UploadImgs.vue'
 import UploadFile from './src/UploadFile.vue'
 
-export { UploadImg, UploadFile }
+export { UploadImg, UploadImgs, UploadFile }

+ 7 - 32
yudao-ui-admin-vue3/src/components/UploadFile/src/UploadFile.vue

@@ -32,8 +32,8 @@
     </el-upload>
   </div>
 </template>
-<script setup lang="ts">
-import { ref, watch } from 'vue'
+<script setup lang="ts" name="UploadFile">
+import { PropType, ref } from 'vue'
 import { useMessage } from '@/hooks/web/useMessage'
 import { propTypes } from '@/utils/propTypes'
 import { getAccessToken, getTenantId } from '@/utils/auth'
@@ -43,7 +43,10 @@ const message = useMessage() // 消息弹窗
 const emit = defineEmits(['update:modelValue'])
 
 const props = defineProps({
-  modelValue: propTypes.oneOfType([String, Object, Array]),
+  modelValue: {
+    type: Array as PropType<UploadUserFile[]>,
+    required: true
+  },
   title: propTypes.string.def('文件上传'),
   updateUrl: propTypes.string.def(import.meta.env.VITE_UPLOAD_URL),
   fileType: propTypes.array.def(['doc', 'xls', 'ppt', 'txt', 'pdf']), // 文件类型, 例如['png', 'jpg', 'jpeg']
@@ -57,40 +60,12 @@ const props = defineProps({
 const valueRef = ref(props.modelValue)
 const uploadRef = ref<UploadInstance>()
 const uploadList = ref<UploadUserFile[]>([])
-const fileList = ref<UploadUserFile[]>([])
+const fileList = ref<UploadUserFile[]>(props.modelValue)
 const uploadNumber = ref<number>(0)
 const uploadHeaders = ref({
   Authorization: 'Bearer ' + getAccessToken(),
   'tenant-id': getTenantId()
 })
-watch(
-  () => props.modelValue,
-  (val) => {
-    if (val) {
-      // 首先将值转为数组, 当只穿了一个图片时,会报map方法错误
-      const list = Array.isArray(props.modelValue)
-        ? props.modelValue
-        : Array.isArray(props.modelValue?.split(','))
-        ? props.modelValue?.split(',')
-        : Array.of(props.modelValue)
-      // 然后将数组转为对象数组
-      fileList.value = list.map((item) => {
-        if (typeof item === 'string') {
-          // edit by 芋道源码
-          item = { name: item, url: item }
-        }
-        return item
-      })
-    } else {
-      fileList.value = []
-      return []
-    }
-  },
-  {
-    deep: true,
-    immediate: true
-  }
-)
 // 文件上传之前判断
 const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => {
   if (fileList.value.length >= props.limit) {

+ 236 - 145
yudao-ui-admin-vue3/src/components/UploadFile/src/UploadImg.vue

@@ -1,176 +1,267 @@
 <template>
-  <div class="component-upload-image">
+  <div class="upload-box">
     <el-upload
-      ref="uploadRef"
-      :multiple="props.limit > 1"
-      name="file"
-      v-model="valueRef"
-      list-type="picture-card"
-      v-model:file-list="fileList"
-      :show-file-list="true"
       :action="updateUrl"
+      :id="uuid"
+      :class="['upload', drag ? 'no-border' : '']"
+      :multiple="false"
+      :show-file-list="false"
       :headers="uploadHeaders"
-      :limit="props.limit"
       :before-upload="beforeUpload"
-      :on-exceed="handleExceed"
-      :on-success="handleFileSuccess"
-      :on-error="excelUploadError"
-      :on-remove="handleRemove"
-      :on-preview="handlePictureCardPreview"
-      :class="{ hide: fileList.length >= props.limit }"
+      :on-success="uploadSuccess"
+      :on-error="uploadError"
+      :drag="drag"
+      :accept="fileType.join(',')"
     >
-      <Icon icon="ep:upload-filled" />
+      <template v-if="modelValue">
+        <img :src="modelValue" class="upload-image" />
+        <div class="upload-handle" @click.stop>
+          <div class="handle-icon" @click="editImg">
+            <Icon icon="ep:edit" />
+            <span>{{ t('action.edit') }}</span>
+          </div>
+          <div class="handle-icon" @click="imgViewVisible = true">
+            <Icon icon="ep:zoom-in" />
+            <span>{{ t('action.detail') }}</span>
+          </div>
+          <div class="handle-icon" @click="deleteImg">
+            <Icon icon="ep:delete" />
+            <span>{{ t('action.del') }}</span>
+          </div>
+        </div>
+      </template>
+      <template v-else>
+        <div class="upload-empty">
+          <slot name="empty">
+            <Icon icon="ep:plus" />
+            <!-- <span>请上传图片</span> -->
+          </slot>
+        </div>
+      </template>
     </el-upload>
+    <div class="el-upload__tip">
+      <slot name="tip"></slot>
+    </div>
+    <el-image-viewer
+      v-if="imgViewVisible"
+      @close="imgViewVisible = false"
+      :url-list="[modelValue]"
+    />
   </div>
-  <!-- 文件列表 -->
-  <Dialog v-model="dialogVisible" title="预览" width="800" append-to-body>
-    <img :src="dialogImageUrl" style="display: block; max-width: 100%; margin: 0 auto" />
-  </Dialog>
 </template>
-<script setup lang="ts">
-import { ref, watch } from 'vue'
-import { Dialog } from '@/components/Dialog'
+
+<script setup lang="ts" name="UploadImg">
+import { ref } from 'vue'
+import type { UploadProps } from 'element-plus'
+import { ElUpload, ElNotification, ElImageViewer } from 'element-plus'
+import { useI18n } from '@/hooks/web/useI18n'
 import { useMessage } from '@/hooks/web/useMessage'
+import { generateUUID } from '@/utils'
 import { propTypes } from '@/utils/propTypes'
 import { getAccessToken, getTenantId } from '@/utils/auth'
-import { ElUpload, UploadInstance, UploadProps, UploadRawFile, UploadUserFile } from 'element-plus'
 
-const message = useMessage() // 消息弹窗
-const emit = defineEmits(['update:modelValue'])
+type FileTypes =
+  | 'image/apng'
+  | 'image/bmp'
+  | 'image/gif'
+  | 'image/jpeg'
+  | 'image/pjpeg'
+  | 'image/png'
+  | 'image/svg+xml'
+  | 'image/tiff'
+  | 'image/webp'
+  | 'image/x-icon'
 
+// 接受父组件参数
 const props = defineProps({
-  modelValue: propTypes.oneOfType([String, Object, Array]),
-  title: propTypes.string.def('图片上传'),
+  modelValue: propTypes.string.def(''),
   updateUrl: propTypes.string.def(import.meta.env.VITE_UPLOAD_URL),
-  fileType: propTypes.array.def(['jpg', 'png', 'gif', 'jpeg']), // 文件类型, 例如['png', 'jpg', 'jpeg']
-  fileSize: propTypes.number.def(5), // 大小限制(MB)
-  limit: propTypes.number.def(1), // 数量限制
-  isShowTip: propTypes.bool.def(false) // 是否显示提示
+  drag: propTypes.bool.def(true), // 是否支持拖拽上传 ==> 非必传(默认为 true)
+  disabled: propTypes.bool.def(false), // 是否禁用上传组件 ==> 非必传(默认为 false)
+  fileSize: propTypes.number.def(5), // 图片大小限制 ==> 非必传(默认为 5M)
+  fileType: propTypes.array.def(['image/jpeg', 'image/png', 'image/gif']), // 图片类型限制 ==> 非必传(默认为 ["image/jpeg", "image/png", "image/gif"])
+  height: propTypes.string.def('150px'), // 组件高度 ==> 非必传(默认为 150px)
+  width: propTypes.string.def('150px'), // 组件宽度 ==> 非必传(默认为 150px)
+  borderRadius: propTypes.string.def('8px') // 组件边框圆角 ==> 非必传(默认为 8px)
 })
-// ========== 上传相关 ==========
-const valueRef = ref(props.modelValue)
-const uploadRef = ref<UploadInstance>()
-const uploadList = ref<UploadUserFile[]>([])
-const fileList = ref<UploadUserFile[]>([])
-const uploadNumber = ref<number>(0)
-const dialogImageUrl = ref()
-const dialogVisible = ref(false)
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+// 生成组件唯一id
+const uuid = ref('id-' + generateUUID())
+// 查看图片
+const imgViewVisible = ref(false)
+
+const emit = defineEmits(['update:modelValue'])
+
+const deleteImg = () => {
+  emit('update:modelValue', '')
+}
+
 const uploadHeaders = ref({
   Authorization: 'Bearer ' + getAccessToken(),
   'tenant-id': getTenantId()
 })
-watch(
-  () => props.modelValue,
-  (val) => {
-    if (val) {
-      // 首先将值转为数组, 当只穿了一个图片时,会报map方法错误
-      const list = Array.isArray(props.modelValue)
-        ? props.modelValue
-        : Array.isArray(props.modelValue?.split(','))
-        ? props.modelValue?.split(',')
-        : Array.of(props.modelValue)
-      // 然后将数组转为对象数组
-      fileList.value = list.map((item) => {
-        if (typeof item === 'string') {
-          // edit by 芋道源码
-          item = { name: item, url: item }
-        }
-        return item
-      })
-    } else {
-      fileList.value = []
-      return []
-    }
-  },
-  {
-    deep: true,
-    immediate: true
-  }
-)
-// 文件上传之前判断
-const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => {
-  if (fileList.value.length >= props.limit) {
-    message.error(`上传文件数量不能超过${props.limit}个!`)
-    return false
-  }
-  let fileExtension = ''
-  if (file.name.lastIndexOf('.') > -1) {
-    fileExtension = file.name.slice(file.name.lastIndexOf('.') + 1)
-  }
-  const isImg = props.fileType.some((type: string) => {
-    if (file.type.indexOf(type) > -1) return true
-    return !!(fileExtension && fileExtension.indexOf(type) > -1)
-  })
-  const isLimit = file.size < props.fileSize * 1024 * 1024
-  if (!isImg) {
-    message.error(`文件格式不正确, 请上传${props.fileType.join('/')}格式!`)
-    return false
-  }
-  if (!isLimit) {
-    message.error(`上传文件大小不能超过${props.fileSize}MB!`)
-    return false
-  }
-  message.success('正在上传文件,请稍候...')
-  uploadNumber.value++
-}
-// 处理上传的文件发生变化
-// const handleFileChange = (uploadFile: UploadFile): void => {
-//   uploadRef.value.data.path = uploadFile.name
-// }
-// 文件上传成功
-const handleFileSuccess: UploadProps['onSuccess'] = (res: any): void => {
-  message.success('上传成功')
-  uploadList.value.push({ name: res.data, url: res.data })
-  if (uploadList.value.length == uploadNumber.value) {
-    fileList.value = fileList.value.concat(uploadList.value)
-    uploadList.value = []
-    uploadNumber.value = 0
-    emit('update:modelValue', listToString(fileList.value))
-  }
-}
-// 文件数超出提示
-const handleExceed: UploadProps['onExceed'] = (): void => {
-  message.error(`上传文件数量不能超过${props.limit}个!`)
-}
-// 上传错误提示
-const excelUploadError: UploadProps['onError'] = (): void => {
-  message.error('导入数据失败,请您重新上传!')
+
+const editImg = () => {
+  const dom = document.querySelector(`#${uuid.value} .el-upload__input`)
+  dom && dom.dispatchEvent(new MouseEvent('click'))
 }
-// 删除上传文件
-const handleRemove = (file) => {
-  const findex = fileList.value.map((f) => f.name).indexOf(file.name)
-  if (findex > -1) {
-    fileList.value.splice(findex, 1)
-    emit('update:modelValue', listToString(fileList.value))
-  }
+
+const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
+  const imgSize = rawFile.size / 1024 / 1024 < props.fileSize
+  const imgType = props.fileType
+  if (!imgType.includes(rawFile.type as FileTypes))
+    ElNotification({
+      title: '温馨提示',
+      message: '上传图片不符合所需的格式!',
+      type: 'warning'
+    })
+  if (!imgSize)
+    ElNotification({
+      title: '温馨提示',
+      message: `上传图片大小不能超过 ${props.fileSize}M!`,
+      type: 'warning'
+    })
+  return imgType.includes(rawFile.type as FileTypes) && imgSize
 }
-// 对象转成指定字符串分隔
-const listToString = (list: UploadUserFile[], separator?: string) => {
-  let strs = ''
-  separator = separator || ','
-  for (let i in list) {
-    strs += list[i].url + separator
-  }
-  return strs != '' ? strs.substr(0, strs.length - 1) : ''
+
+// 图片上传成功提示
+const uploadSuccess: UploadProps['onSuccess'] = (res: any): void => {
+  message.success('上传成功')
+  emit('update:modelValue', res.data)
 }
-// 预览
-const handlePictureCardPreview: UploadProps['onPreview'] = (file) => {
-  dialogImageUrl.value = file.url
-  dialogVisible.value = true
+
+// 图片上传错误提示
+const uploadError = () => {
+  ElNotification({
+    title: '温馨提示',
+    message: '图片上传失败,请您重新上传!',
+    type: 'error'
+  })
 }
 </script>
 <style scoped lang="scss">
-// .el-upload--picture-card 控制加号部分
-:deep(.hide .el-upload--picture-card) {
-  display: none;
+.is-error {
+  .upload {
+    :deep(.el-upload),
+    :deep(.el-upload-dragger) {
+      border: 1px dashed var(--el-color-danger) !important;
+      &:hover {
+        border-color: var(--el-color-primary) !important;
+      }
+    }
+  }
 }
-// 去掉动画效果
-:deep(.el-list-enter-active, .el-list-leave-active) {
-  transition: all 0s;
+:deep(.disabled) {
+  .el-upload,
+  .el-upload-dragger {
+    cursor: not-allowed !important;
+    background: var(--el-disabled-bg-color);
+    border: 1px dashed var(--el-border-color-darker) !important;
+    &:hover {
+      border: 1px dashed var(--el-border-color-darker) !important;
+    }
+  }
 }
-
-:deep(.el-list-enter, .el-list-leave-active) {
-  opacity: 0;
-  transform: translateY(0);
+.upload-box {
+  .no-border {
+    :deep(.el-upload) {
+      border: none !important;
+    }
+  }
+  :deep(.upload) {
+    .el-upload {
+      position: relative;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: v-bind(width);
+      height: v-bind(height);
+      overflow: hidden;
+      border: 1px dashed var(--el-border-color-darker);
+      border-radius: v-bind(borderRadius);
+      transition: var(--el-transition-duration-fast);
+      &:hover {
+        border-color: var(--el-color-primary);
+        .upload-handle {
+          opacity: 1;
+        }
+      }
+      .el-upload-dragger {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        width: 100%;
+        height: 100%;
+        padding: 0;
+        overflow: hidden;
+        background-color: transparent;
+        border: 1px dashed var(--el-border-color-darker);
+        border-radius: v-bind(borderRadius);
+        &:hover {
+          border: 1px dashed var(--el-color-primary);
+        }
+      }
+      .el-upload-dragger.is-dragover {
+        background-color: var(--el-color-primary-light-9);
+        border: 2px dashed var(--el-color-primary) !important;
+      }
+      .upload-image {
+        width: 100%;
+        height: 100%;
+        object-fit: contain;
+      }
+      .upload-empty {
+        position: relative;
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        justify-content: center;
+        font-size: 12px;
+        line-height: 30px;
+        color: var(--el-color-info);
+        .el-icon {
+          font-size: 28px;
+          color: var(--el-text-color-secondary);
+        }
+      }
+      .upload-handle {
+        position: absolute;
+        top: 0;
+        right: 0;
+        box-sizing: border-box;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        width: 100%;
+        height: 100%;
+        cursor: pointer;
+        background: rgb(0 0 0 / 60%);
+        opacity: 0;
+        transition: var(--el-transition-duration-fast);
+        .handle-icon {
+          display: flex;
+          flex-direction: column;
+          align-items: center;
+          justify-content: center;
+          padding: 0 6%;
+          color: aliceblue;
+          .el-icon {
+            margin-bottom: 40%;
+            font-size: 130%;
+            line-height: 130%;
+          }
+          span {
+            font-size: 85%;
+            line-height: 85%;
+          }
+        }
+      }
+    }
+  }
+  .el-upload__tip {
+    line-height: 18px;
+    text-align: center;
+  }
 }
 </style>

+ 277 - 0
yudao-ui-admin-vue3/src/components/UploadFile/src/UploadImgs.vue

@@ -0,0 +1,277 @@
+<template>
+  <div class="upload-box">
+    <el-upload
+      :action="updateUrl"
+      list-type="picture-card"
+      :class="['upload', drag ? 'no-border' : '']"
+      v-model:file-list="fileList"
+      :multiple="true"
+      :limit="limit"
+      :headers="uploadHeaders"
+      :before-upload="beforeUpload"
+      :on-exceed="handleExceed"
+      :on-success="uploadSuccess"
+      :on-error="uploadError"
+      :drag="drag"
+      :accept="fileType.join(',')"
+    >
+      <div class="upload-empty">
+        <slot name="empty">
+          <Icon icon="ep:plus" />
+          <!-- <span>请上传图片</span> -->
+        </slot>
+      </div>
+      <template #file="{ file }">
+        <img :src="file.url" class="upload-image" />
+        <div class="upload-handle" @click.stop>
+          <div class="handle-icon" @click="handlePictureCardPreview(file)">
+            <Icon icon="ep:zoom-in" />
+            <span>查看</span>
+          </div>
+          <div class="handle-icon" @click="handleRemove(file)">
+            <Icon icon="ep:delete" />
+            <span>删除</span>
+          </div>
+        </div>
+      </template>
+    </el-upload>
+    <div class="el-upload__tip">
+      <slot name="tip"></slot>
+    </div>
+    <el-image-viewer
+      v-if="imgViewVisible"
+      @close="imgViewVisible = false"
+      :url-list="[viewImageUrl]"
+    />
+  </div>
+</template>
+<script setup lang="ts" name="UploadImgs">
+import { PropType, ref } from 'vue'
+import { ElUpload, ElNotification, ElImageViewer } from 'element-plus'
+import type { UploadProps, UploadFile, UploadUserFile } from 'element-plus'
+import { useMessage } from '@/hooks/web/useMessage'
+import { propTypes } from '@/utils/propTypes'
+import { getAccessToken, getTenantId } from '@/utils/auth'
+
+const message = useMessage() // 消息弹窗
+
+type FileTypes =
+  | 'image/apng'
+  | 'image/bmp'
+  | 'image/gif'
+  | 'image/jpeg'
+  | 'image/pjpeg'
+  | 'image/png'
+  | 'image/svg+xml'
+  | 'image/tiff'
+  | 'image/webp'
+  | 'image/x-icon'
+
+const props = defineProps({
+  modelValue: {
+    type: Array as PropType<UploadUserFile[]>,
+    required: true
+  },
+  updateUrl: propTypes.string.def(import.meta.env.VITE_UPLOAD_URL),
+  drag: propTypes.bool.def(true), // 是否支持拖拽上传 ==> 非必传(默认为 true)
+  disabled: propTypes.bool.def(false), // 是否禁用上传组件 ==> 非必传(默认为 false)
+  limit: propTypes.number.def(5), // 最大图片上传数 ==> 非必传(默认为 5张)
+  fileSize: propTypes.number.def(5), // 图片大小限制 ==> 非必传(默认为 5M)
+  fileType: propTypes.array.def(['image/jpeg', 'image/png', 'image/gif']), // 图片类型限制 ==> 非必传(默认为 ["image/jpeg", "image/png", "image/gif"])
+  height: propTypes.string.def('150px'), // 组件高度 ==> 非必传(默认为 150px)
+  width: propTypes.string.def('150px'), // 组件宽度 ==> 非必传(默认为 150px)
+  borderRadius: propTypes.string.def('8px') // 组件边框圆角 ==> 非必传(默认为 8px)
+})
+
+const uploadHeaders = ref({
+  Authorization: 'Bearer ' + getAccessToken(),
+  'tenant-id': getTenantId()
+})
+
+const fileList = ref<UploadUserFile[]>(props.modelValue)
+
+/**
+ * @description 文件上传之前判断
+ * @param rawFile 上传的文件
+ * */
+const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
+  const imgSize = rawFile.size / 1024 / 1024 < props.fileSize
+  const imgType = props.fileType
+  if (!imgType.includes(rawFile.type as FileTypes))
+    ElNotification({
+      title: '温馨提示',
+      message: '上传图片不符合所需的格式!',
+      type: 'warning'
+    })
+  if (!imgSize)
+    ElNotification({
+      title: '温馨提示',
+      message: `上传图片大小不能超过 ${props.fileSize}M!`,
+      type: 'warning'
+    })
+  return imgType.includes(rawFile.type as FileTypes) && imgSize
+}
+
+// 图片上传成功
+interface UploadEmits {
+  (e: 'update:modelValue', value: UploadUserFile[]): void
+}
+const emit = defineEmits<UploadEmits>()
+const uploadSuccess = (response, uploadFile: UploadFile) => {
+  if (!response) return
+  uploadFile.url = response.data
+  emit('update:modelValue', fileList.value)
+  message.success('上传成功')
+}
+
+// 删除图片
+const handleRemove = (uploadFile: UploadFile) => {
+  fileList.value = fileList.value.filter(
+    (item) => item.url !== uploadFile.url || item.name !== uploadFile.name
+  )
+  emit('update:modelValue', fileList.value)
+}
+
+// 图片上传错误提示
+const uploadError = () => {
+  ElNotification({
+    title: '温馨提示',
+    message: '图片上传失败,请您重新上传!',
+    type: 'error'
+  })
+}
+
+// 文件数超出提示
+const handleExceed = () => {
+  ElNotification({
+    title: '温馨提示',
+    message: `当前最多只能上传 ${props.limit} 张图片,请移除后上传!`,
+    type: 'warning'
+  })
+}
+
+// 图片预览
+const viewImageUrl = ref('')
+const imgViewVisible = ref(false)
+const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
+  viewImageUrl.value = uploadFile.url!
+  imgViewVisible.value = true
+}
+</script>
+
+<style scoped lang="scss">
+.is-error {
+  .upload {
+    :deep(.el-upload--picture-card),
+    :deep(.el-upload-dragger) {
+      border: 1px dashed var(--el-color-danger) !important;
+      &:hover {
+        border-color: var(--el-color-primary) !important;
+      }
+    }
+  }
+}
+:deep(.disabled) {
+  .el-upload--picture-card,
+  .el-upload-dragger {
+    cursor: not-allowed;
+    background: var(--el-disabled-bg-color) !important;
+    border: 1px dashed var(--el-border-color-darker);
+    &:hover {
+      border-color: var(--el-border-color-darker) !important;
+    }
+  }
+}
+.upload-box {
+  .no-border {
+    :deep(.el-upload--picture-card) {
+      border: none !important;
+    }
+  }
+  :deep(.upload) {
+    .el-upload-dragger {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 100%;
+      height: 100%;
+      padding: 0;
+      overflow: hidden;
+      border: 1px dashed var(--el-border-color-darker);
+      border-radius: v-bind(borderRadius);
+      &:hover {
+        border: 1px dashed var(--el-color-primary);
+      }
+    }
+    .el-upload-dragger.is-dragover {
+      background-color: var(--el-color-primary-light-9);
+      border: 2px dashed var(--el-color-primary) !important;
+    }
+    .el-upload-list__item,
+    .el-upload--picture-card {
+      width: v-bind(width);
+      height: v-bind(height);
+      background-color: transparent;
+      border-radius: v-bind(borderRadius);
+    }
+    .upload-image {
+      width: 100%;
+      height: 100%;
+      object-fit: contain;
+    }
+    .upload-handle {
+      position: absolute;
+      top: 0;
+      right: 0;
+      box-sizing: border-box;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 100%;
+      height: 100%;
+      cursor: pointer;
+      background: rgb(0 0 0 / 60%);
+      opacity: 0;
+      transition: var(--el-transition-duration-fast);
+      .handle-icon {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        justify-content: center;
+        padding: 0 6%;
+        color: aliceblue;
+        .el-icon {
+          margin-bottom: 15%;
+          font-size: 140%;
+        }
+        span {
+          font-size: 100%;
+        }
+      }
+    }
+    .el-upload-list__item {
+      &:hover {
+        .upload-handle {
+          opacity: 1;
+        }
+      }
+    }
+    .upload-empty {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      font-size: 12px;
+      line-height: 30px;
+      color: var(--el-color-info);
+      .el-icon {
+        font-size: 28px;
+        color: var(--el-text-color-secondary);
+      }
+    }
+  }
+  .el-upload__tip {
+    line-height: 15px;
+    text-align: center;
+  }
+}
+</style>

+ 1 - 0
yudao-ui-admin-vue3/src/types/components.d.ts

@@ -22,6 +22,7 @@ export type ComponentName =
   | 'InputPassword'
   | 'Editor'
   | 'UploadImg'
+  | 'UploadImgs'
   | 'UploadFile'
 
 export type ColProps = {

+ 33 - 2
yudao-ui-admin-vue3/src/utils/index.ts

@@ -69,7 +69,7 @@ export const trim = (str: string) => {
  * @param {Date | number | string} time 需要转换的时间
  * @param {String} fmt 需要转换的格式 如 yyyy-MM-dd、yyyy-MM-dd HH:mm:ss
  */
-export function formatTime(time: Date | number | string, fmt: string) {
+export const formatTime = (time: Date | number | string, fmt: string) => {
   if (!time) return ''
   else {
     const date = new Date(time)
@@ -100,7 +100,7 @@ export function formatTime(time: Date | number | string, fmt: string) {
 /**
  * 生成随机字符串
  */
-export function toAnyString() {
+export const toAnyString = () => {
   const str: string = 'xxxxx-xxxxx-4xxxx-yxxxx-xxxxx'.replace(/[xy]/g, (c: string) => {
     const r: number = (Math.random() * 16) | 0
     const v: number = c === 'x' ? r : (r & 0x3) | 0x8
@@ -108,3 +108,34 @@ export function toAnyString() {
   })
   return str
 }
+
+export const generateUUID = () => {
+  if (typeof crypto === 'object') {
+    if (typeof crypto.randomUUID === 'function') {
+      return crypto.randomUUID()
+    }
+    if (typeof crypto.getRandomValues === 'function' && typeof Uint8Array === 'function') {
+      const callback = (c: any) => {
+        const num = Number(c)
+        return (num ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (num / 4)))).toString(
+          16
+        )
+      }
+      return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, callback)
+    }
+  }
+  let timestamp = new Date().getTime()
+  let performanceNow =
+    (typeof performance !== 'undefined' && performance.now && performance.now() * 1000) || 0
+  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
+    let random = Math.random() * 16
+    if (timestamp > 0) {
+      random = (timestamp + random) % 16 | 0
+      timestamp = Math.floor(timestamp / 16)
+    } else {
+      random = (performanceNow + random) % 16 | 0
+      performanceNow = Math.floor(performanceNow / 16)
+    }
+    return (c === 'x' ? random : (random & 0x3) | 0x8).toString(16)
+  })
+}

+ 1 - 4
yudao-ui-admin-vue3/src/views/system/oauth2/client/client.data.ts

@@ -43,10 +43,7 @@ const crudSchemas = reactive<VxeCrudSchema>({
         }
       },
       form: {
-        component: 'UploadImg',
-        componentProps: {
-          limit: 1
-        }
+        component: 'UploadImg'
       }
     },
     {