Commit 9de323e09ca121bc24ff5fb21771d766f107036c

Authored by xingyu
1 parent 2d791301

perf: 完善上传组件

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&lt;Component, ComponentName&gt; = { @@ -48,6 +48,7 @@ const componentMap: Recordable&lt;Component, ComponentName&gt; = {
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) =&gt; { @@ -69,7 +69,7 @@ export const trim = (str: string) =&gt; {
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&lt;VxeCrudSchema&gt;({ @@ -43,10 +43,7 @@ const crudSchemas = reactive&lt;VxeCrudSchema&gt;({
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 {