Commit 731e49d7b6fe37258b7c0d5894ce129b5a192c9c

Authored by xingyu
1 parent ba6a2957

feat: add upload component

yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/data.ts.vm
@@ -77,6 +77,17 @@ const crudSchemas = reactive<VxeCrudSchema>({ @@ -77,6 +77,17 @@ const crudSchemas = reactive<VxeCrudSchema>({
77 component: 'InputNumber', 77 component: 'InputNumber',
78 value: 0 78 value: 0
79 }, 79 },
  80 + #elseif($column.htmlType == "imageUpload")## 图片上传
  81 + form: {
  82 + component: 'UploadImg',
  83 + componentProps: {
  84 + limit: 1
  85 + }
  86 + },
  87 + #elseif($column.htmlType == "fileUpload")## 图片上传
  88 + form: {
  89 + component: 'UploadFile'
  90 + },
80 #end 91 #end
81 #end 92 #end
82 #if ($column.listOperation) 93 #if ($column.listOperation)
yudao-ui-admin-vue3/src/components/Form/src/componentMap.ts
@@ -21,6 +21,7 @@ import { @@ -21,6 +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 { ComponentName } from '@/types/components' 25 import { ComponentName } from '@/types/components'
25 26
26 const componentMap: Recordable<Component, ComponentName> = { 27 const componentMap: Recordable<Component, ComponentName> = {
@@ -45,7 +46,9 @@ const componentMap: Recordable&lt;Component, ComponentName&gt; = { @@ -45,7 +46,9 @@ const componentMap: Recordable&lt;Component, ComponentName&gt; = {
45 TreeSelect: ElTreeSelect, 46 TreeSelect: ElTreeSelect,
46 RadioButton: ElRadioGroup, 47 RadioButton: ElRadioGroup,
47 InputPassword: InputPassword, 48 InputPassword: InputPassword,
48 - Editor: Editor 49 + Editor: Editor,
  50 + UploadImg: UploadImg,
  51 + UploadFile: UploadFile
49 } 52 }
50 53
51 export { componentMap } 54 export { componentMap }
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 UploadFile from './src/UploadFile.vue'
2 3
3 -export { UploadImg } 4 +export { UploadImg, UploadFile }
yudao-ui-admin-vue3/src/components/UploadFile/src/UploadFile.vue 0 → 100644
  1 +<template>
  2 + <div class="upload-file">
  3 + <el-upload
  4 + ref="uploadRef"
  5 + :multiple="props.limit > 1"
  6 + name="file"
  7 + v-model="valueRef"
  8 + :file-list="fileList"
  9 + :show-file-list="false"
  10 + :action="updateUrl"
  11 + :headers="uploadHeaders"
  12 + :limit="props.limit"
  13 + :before-upload="beforeUpload"
  14 + :on-exceed="handleExceed"
  15 + :on-success="handleFileSuccess"
  16 + :on-error="excelUploadError"
  17 + :on-remove="handleRemove"
  18 + class="upload-file-uploader"
  19 + >
  20 + <Icon icon="ep:upload-filled" />
  21 + </el-upload>
  22 + </div>
  23 +</template>
  24 +<script setup lang="ts">
  25 +import { ref, watch } from 'vue'
  26 +import { useMessage } from '@/hooks/web/useMessage'
  27 +import { propTypes } from '@/utils/propTypes'
  28 +import { getAccessToken, getTenantId } from '@/utils/auth'
  29 +import { ElUpload, UploadInstance, UploadProps, UploadRawFile, UploadUserFile } from 'element-plus'
  30 +
  31 +const message = useMessage() // 消息弹窗
  32 +const emit = defineEmits(['update:modelValue'])
  33 +
  34 +const props = defineProps({
  35 + modelValue: propTypes.oneOfType([String, Object, Array]),
  36 + title: propTypes.string.def('文件上传'),
  37 + updateUrl: propTypes.string.def(import.meta.env.VITE_UPLOAD_URL),
  38 + fileType: propTypes.array.def(['doc', 'xls', 'ppt', 'txt', 'pdf']), // 文件类型, 例如['png', 'jpg', 'jpeg']
  39 + fileSize: propTypes.number.def(5), // 大小限制(MB)
  40 + limit: propTypes.number.def(5), // 数量限制
  41 + isShowTip: propTypes.bool.def(false) // 是否显示提示
  42 +})
  43 +// ========== 上传相关 ==========
  44 +const valueRef = ref(props.modelValue)
  45 +const uploadRef = ref<UploadInstance>()
  46 +const uploadList = ref<UploadUserFile[]>([])
  47 +const fileList = ref<UploadUserFile[]>([])
  48 +const uploadNumber = ref<number>(0)
  49 +const uploadHeaders = ref({
  50 + Authorization: 'Bearer ' + getAccessToken(),
  51 + 'tenant-id': getTenantId()
  52 +})
  53 +watch(
  54 + () => props.modelValue,
  55 + (val) => {
  56 + if (val) {
  57 + // 首先将值转为数组, 当只穿了一个图片时,会报map方法错误
  58 + const list = Array.isArray(props.modelValue)
  59 + ? props.modelValue
  60 + : Array.isArray(props.modelValue?.split(','))
  61 + ? props.modelValue?.split(',')
  62 + : Array.of(props.modelValue)
  63 + // 然后将数组转为对象数组
  64 + fileList.value = list.map((item) => {
  65 + if (typeof item === 'string') {
  66 + // edit by 芋道源码
  67 + item = { name: item, url: item }
  68 + }
  69 + return item
  70 + })
  71 + } else {
  72 + fileList.value = []
  73 + return []
  74 + }
  75 + },
  76 + {
  77 + deep: true,
  78 + immediate: true
  79 + }
  80 +)
  81 +// 文件上传之前判断
  82 +const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => {
  83 + if (fileList.value.length >= props.limit) {
  84 + message.error(`上传文件数量不能超过${props.limit}个!`)
  85 + return false
  86 + }
  87 + let fileExtension = ''
  88 + if (file.name.lastIndexOf('.') > -1) {
  89 + fileExtension = file.name.slice(file.name.lastIndexOf('.') + 1)
  90 + }
  91 + const isImg = props.fileType.some((type: string) => {
  92 + if (file.type.indexOf(type) > -1) return true
  93 + return !!(fileExtension && fileExtension.indexOf(type) > -1)
  94 + })
  95 + const isLimit = file.size < props.fileSize * 1024 * 1024
  96 + if (!isImg) {
  97 + message.error(`文件格式不正确, 请上传${props.fileType.join('/')}格式!`)
  98 + return false
  99 + }
  100 + if (!isLimit) {
  101 + message.error(`上传文件大小不能超过${props.fileSize}MB!`)
  102 + return false
  103 + }
  104 + message.success('正在上传文件,请稍候...')
  105 + uploadNumber.value++
  106 +}
  107 +// 处理上传的文件发生变化
  108 +// const handleFileChange = (uploadFile: UploadFile): void => {
  109 +// uploadRef.value.data.path = uploadFile.name
  110 +// }
  111 +// 文件上传成功
  112 +const handleFileSuccess: UploadProps['onSuccess'] = (res: any): void => {
  113 + message.success('上传成功')
  114 + uploadList.value.push({ name: res.data, url: res.data })
  115 + if (uploadList.value.length == uploadNumber.value) {
  116 + fileList.value = fileList.value.concat(uploadList.value)
  117 + uploadList.value = []
  118 + uploadNumber.value = 0
  119 + emit('update:modelValue', listToString(fileList.value))
  120 + }
  121 +}
  122 +// 文件数超出提示
  123 +const handleExceed: UploadProps['onExceed'] = (): void => {
  124 + message.error(`上传文件数量不能超过${props.limit}个!`)
  125 +}
  126 +// 上传错误提示
  127 +const excelUploadError: UploadProps['onError'] = (): void => {
  128 + message.error('导入数据失败,请您重新上传!')
  129 +}
  130 +// 删除上传文件
  131 +const handleRemove = (file) => {
  132 + const findex = fileList.value.map((f) => f.name).indexOf(file.name)
  133 + if (findex > -1) {
  134 + fileList.value.splice(findex, 1)
  135 + emit('update:modelValue', listToString(fileList.value))
  136 + }
  137 +}
  138 +// 对象转成指定字符串分隔
  139 +const listToString = (list: UploadUserFile[], separator?: string) => {
  140 + let strs = ''
  141 + separator = separator || ','
  142 + for (let i in list) {
  143 + strs += list[i].url + separator
  144 + }
  145 + return strs != '' ? strs.substr(0, strs.length - 1) : ''
  146 +}
  147 +</script>
  148 +<style scoped lang="scss">
  149 +.upload-file-uploader {
  150 + margin-bottom: 5px;
  151 +}
  152 +.upload-file-list .el-upload-list__item {
  153 + border: 1px solid #e4e7ed;
  154 + line-height: 2;
  155 + margin-bottom: 10px;
  156 + position: relative;
  157 +}
  158 +.upload-file-list .ele-upload-list__item-content {
  159 + display: flex;
  160 + justify-content: space-between;
  161 + align-items: center;
  162 + color: inherit;
  163 +}
  164 +.ele-upload-list__item-content-action .el-link {
  165 + margin-right: 10px;
  166 +}
  167 +</style>
yudao-ui-admin-vue3/src/components/UploadFile/src/UploadImg.vue
1 <template> 1 <template>
2 - <el-upload  
3 - ref="uploadRef"  
4 - :multiple="limit > 1"  
5 - name="file"  
6 - list-type="picture-card"  
7 - v-model:file-list="fileList"  
8 - :show-file-list="true"  
9 - :action="updateUrl"  
10 - :headers="uploadHeaders"  
11 - :limit="limit"  
12 - :before-upload="beforeUpload"  
13 - :on-exceed="handleExceed"  
14 - :on-success="handleFileSuccess"  
15 - :on-error="excelUploadError"  
16 - :on-remove="handleRemove"  
17 - :on-preview="handlePictureCardPreview"  
18 - :class="{ hide: fileList.length >= limit }"  
19 - >  
20 - <Icon icon="ep:upload-filled" />  
21 - </el-upload> 2 + <div class="component-upload-image">
  3 + <el-upload
  4 + ref="uploadRef"
  5 + :multiple="props.limit > 1"
  6 + name="file"
  7 + v-model="valueRef"
  8 + list-type="picture-card"
  9 + :file-list="fileList"
  10 + :show-file-list="true"
  11 + :action="updateUrl"
  12 + :headers="uploadHeaders"
  13 + :limit="props.limit"
  14 + :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 }"
  21 + >
  22 + <Icon icon="ep:upload-filled" />
  23 + </el-upload>
  24 + </div>
22 <!-- 文件列表 --> 25 <!-- 文件列表 -->
23 <Dialog v-model="dialogVisible" title="预览" width="800" append-to-body> 26 <Dialog v-model="dialogVisible" title="预览" width="800" append-to-body>
24 <img :src="dialogImageUrl" style="display: block; max-width: 100%; margin: 0 auto" /> 27 <img :src="dialogImageUrl" style="display: block; max-width: 100%; margin: 0 auto" />
@@ -32,10 +35,10 @@ import { getAccessToken, getTenantId } from &#39;@/utils/auth&#39; @@ -32,10 +35,10 @@ import { getAccessToken, getTenantId } from &#39;@/utils/auth&#39;
32 import { ElUpload, UploadInstance, UploadProps, UploadRawFile, UploadUserFile } from 'element-plus' 35 import { ElUpload, UploadInstance, UploadProps, UploadRawFile, UploadUserFile } from 'element-plus'
33 36
34 const message = useMessage() // 消息弹窗 37 const message = useMessage() // 消息弹窗
35 -const emit = defineEmits(['input']) 38 +const emit = defineEmits(['update:modelValue'])
36 39
37 const props = defineProps({ 40 const props = defineProps({
38 - imgs: propTypes.oneOfType([String, Object, Array]), 41 + modelValue: propTypes.oneOfType([String, Object, Array]),
39 title: propTypes.string.def('图片上传'), 42 title: propTypes.string.def('图片上传'),
40 updateUrl: propTypes.string.def(import.meta.env.VITE_UPLOAD_URL), 43 updateUrl: propTypes.string.def(import.meta.env.VITE_UPLOAD_URL),
41 fileType: propTypes.array.def(['jpg', 'png', 'gif', 'jpeg']), // 文件类型, 例如['png', 'jpg', 'jpeg'] 44 fileType: propTypes.array.def(['jpg', 'png', 'gif', 'jpeg']), // 文件类型, 例如['png', 'jpg', 'jpeg']
@@ -44,6 +47,7 @@ const props = defineProps({ @@ -44,6 +47,7 @@ const props = defineProps({
44 isShowTip: propTypes.bool.def(false) // 是否显示提示 47 isShowTip: propTypes.bool.def(false) // 是否显示提示
45 }) 48 })
46 // ========== 上传相关 ========== 49 // ========== 上传相关 ==========
  50 +const valueRef = ref(props.modelValue)
47 const uploadRef = ref<UploadInstance>() 51 const uploadRef = ref<UploadInstance>()
48 const uploadList = ref<UploadUserFile[]>([]) 52 const uploadList = ref<UploadUserFile[]>([])
49 const fileList = ref<UploadUserFile[]>([]) 53 const fileList = ref<UploadUserFile[]>([])
@@ -55,15 +59,15 @@ const uploadHeaders = ref({ @@ -55,15 +59,15 @@ const uploadHeaders = ref({
55 'tenant-id': getTenantId() 59 'tenant-id': getTenantId()
56 }) 60 })
57 watch( 61 watch(
58 - () => props.imgs, 62 + () => props.modelValue,
59 (val) => { 63 (val) => {
60 if (val) { 64 if (val) {
61 // 首先将值转为数组, 当只穿了一个图片时,会报map方法错误 65 // 首先将值转为数组, 当只穿了一个图片时,会报map方法错误
62 - const list = Array.isArray(props.imgs)  
63 - ? props.imgs  
64 - : Array.isArray(props.imgs?.split(','))  
65 - ? props.imgs?.split(',')  
66 - : Array.of(props.imgs) 66 + const list = Array.isArray(props.modelValue)
  67 + ? props.modelValue
  68 + : Array.isArray(props.modelValue?.split(','))
  69 + ? props.modelValue?.split(',')
  70 + : Array.of(props.modelValue)
67 // 然后将数组转为对象数组 71 // 然后将数组转为对象数组
68 fileList.value = list.map((item) => { 72 fileList.value = list.map((item) => {
69 if (typeof item === 'string') { 73 if (typeof item === 'string') {
@@ -84,6 +88,10 @@ watch( @@ -84,6 +88,10 @@ watch(
84 ) 88 )
85 // 文件上传之前判断 89 // 文件上传之前判断
86 const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => { 90 const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => {
  91 + if (fileList.value.length >= props.limit) {
  92 + message.error(`上传文件数量不能超过${props.limit}个!`)
  93 + return false
  94 + }
87 let fileExtension = '' 95 let fileExtension = ''
88 if (file.name.lastIndexOf('.') > -1) { 96 if (file.name.lastIndexOf('.') > -1) {
89 fileExtension = file.name.slice(file.name.lastIndexOf('.') + 1) 97 fileExtension = file.name.slice(file.name.lastIndexOf('.') + 1)
@@ -111,14 +119,12 @@ const beforeUpload: UploadProps[&#39;beforeUpload&#39;] = (file: UploadRawFile) =&gt; { @@ -111,14 +119,12 @@ const beforeUpload: UploadProps[&#39;beforeUpload&#39;] = (file: UploadRawFile) =&gt; {
111 // 文件上传成功 119 // 文件上传成功
112 const handleFileSuccess: UploadProps['onSuccess'] = (res: any): void => { 120 const handleFileSuccess: UploadProps['onSuccess'] = (res: any): void => {
113 message.success('上传成功') 121 message.success('上传成功')
114 - console.info(uploadList.value)  
115 - console.info(fileList.value)  
116 uploadList.value.push({ name: res.data, url: res.data }) 122 uploadList.value.push({ name: res.data, url: res.data })
117 if (uploadList.value.length == uploadNumber.value) { 123 if (uploadList.value.length == uploadNumber.value) {
118 fileList.value = fileList.value.concat(uploadList.value) 124 fileList.value = fileList.value.concat(uploadList.value)
119 uploadList.value = [] 125 uploadList.value = []
120 uploadNumber.value = 0 126 uploadNumber.value = 0
121 - emit('input', listToString(fileList.value)) 127 + emit('update:modelValue', listToString(fileList.value))
122 } 128 }
123 } 129 }
124 // 文件数超出提示 130 // 文件数超出提示
@@ -134,7 +140,7 @@ const handleRemove = (file) =&gt; { @@ -134,7 +140,7 @@ const handleRemove = (file) =&gt; {
134 const findex = fileList.value.map((f) => f.name).indexOf(file.name) 140 const findex = fileList.value.map((f) => f.name).indexOf(file.name)
135 if (findex > -1) { 141 if (findex > -1) {
136 fileList.value.splice(findex, 1) 142 fileList.value.splice(findex, 1)
137 - emit('input', listToString(fileList.value)) 143 + emit('update:modelValue', listToString(fileList.value))
138 } 144 }
139 } 145 }
140 // 对象转成指定字符串分隔 146 // 对象转成指定字符串分隔
yudao-ui-admin-vue3/src/types/components.d.ts
@@ -21,6 +21,8 @@ export type ComponentName = @@ -21,6 +21,8 @@ export type ComponentName =
21 | 'TreeSelect' 21 | 'TreeSelect'
22 | 'InputPassword' 22 | 'InputPassword'
23 | 'Editor' 23 | 'Editor'
  24 + | 'UploadImg'
  25 + | 'UploadFile'
24 26
25 export type ColProps = { 27 export type ColProps = {
26 span?: number 28 span?: number
yudao-ui-admin-vue3/src/views/system/oauth2/client/client.data.ts
@@ -41,6 +41,12 @@ const crudSchemas = reactive&lt;VxeCrudSchema&gt;({ @@ -41,6 +41,12 @@ const crudSchemas = reactive&lt;VxeCrudSchema&gt;({
41 cellRender: { 41 cellRender: {
42 name: 'XImg' 42 name: 'XImg'
43 } 43 }
  44 + },
  45 + form: {
  46 + component: 'UploadImg',
  47 + componentProps: {
  48 + limit: 1
  49 + }
44 } 50 }
45 }, 51 },
46 { 52 {
yudao-ui-admin-vue3/src/views/system/oauth2/client/index.vue
@@ -61,16 +61,12 @@ @@ -61,16 +61,12 @@
61 v-if="['create', 'update'].includes(actionType)" 61 v-if="['create', 'update'].includes(actionType)"
62 :schema="allSchemas.formSchema" 62 :schema="allSchemas.formSchema"
63 :rules="rules" 63 :rules="rules"
64 - >  
65 - <template #logo="form">  
66 - <UploadImg :imgs="form['logo']" :limit="1" />  
67 - </template>  
68 - </Form> 64 + />
69 <!-- 表单:详情 --> 65 <!-- 表单:详情 -->
70 <Descriptions 66 <Descriptions
71 v-if="actionType === 'detail'" 67 v-if="actionType === 'detail'"
72 :schema="allSchemas.detailSchema" 68 :schema="allSchemas.detailSchema"
73 - :data="detailRef" 69 + :data="detailData"
74 > 70 >
75 <template #accessTokenValiditySeconds="{ row }"> 71 <template #accessTokenValiditySeconds="{ row }">
76 {{ row.accessTokenValiditySeconds + '秒' }} 72 {{ row.accessTokenValiditySeconds + '秒' }}
@@ -142,7 +138,6 @@ import { useMessage } from &#39;@/hooks/web/useMessage&#39; @@ -142,7 +138,6 @@ import { useMessage } from &#39;@/hooks/web/useMessage&#39;
142 import { useVxeGrid } from '@/hooks/web/useVxeGrid' 138 import { useVxeGrid } from '@/hooks/web/useVxeGrid'
143 import { VxeGridInstance } from 'vxe-table' 139 import { VxeGridInstance } from 'vxe-table'
144 import { FormExpose } from '@/components/Form' 140 import { FormExpose } from '@/components/Form'
145 -import { UploadImg } from '@/components/UploadFile'  
146 // 业务相关的 import 141 // 业务相关的 import
147 import * as ClientApi from '@/api/system/oauth2/client' 142 import * as ClientApi from '@/api/system/oauth2/client'
148 import { rules, allSchemas } from './client.data' 143 import { rules, allSchemas } from './client.data'
@@ -163,7 +158,7 @@ const dialogTitle = ref(&#39;edit&#39;) // 弹出层标题 @@ -163,7 +158,7 @@ const dialogTitle = ref(&#39;edit&#39;) // 弹出层标题
163 const actionType = ref('') // 操作按钮的类型 158 const actionType = ref('') // 操作按钮的类型
164 const actionLoading = ref(false) // 按钮 Loading 159 const actionLoading = ref(false) // 按钮 Loading
165 const formRef = ref<FormExpose>() // 表单 Ref 160 const formRef = ref<FormExpose>() // 表单 Ref
166 -const detailRef = ref() // 详情 Ref 161 +const detailData = ref() // 详情 Ref
167 // 设置标题 162 // 设置标题
168 const setDialogTile = (type: string) => { 163 const setDialogTile = (type: string) => {
169 dialogTitle.value = t('action.' + type) 164 dialogTitle.value = t('action.' + type)
@@ -188,7 +183,7 @@ const handleUpdate = async (rowId: number) =&gt; { @@ -188,7 +183,7 @@ const handleUpdate = async (rowId: number) =&gt; {
188 const handleDetail = async (rowId: number) => { 183 const handleDetail = async (rowId: number) => {
189 setDialogTile('detail') 184 setDialogTile('detail')
190 const res = await ClientApi.getOAuth2ClientApi(rowId) 185 const res = await ClientApi.getOAuth2ClientApi(rowId)
191 - detailRef.value = res 186 + detailData.value = res
192 } 187 }
193 188
194 // 删除操作 189 // 删除操作