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 77 component: 'InputNumber',
78 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 91 #end
81 92 #end
82 93 #if ($column.listOperation)
... ...
yudao-ui-admin-vue3/src/components/Form/src/componentMap.ts
... ... @@ -21,6 +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 25 import { ComponentName } from '@/types/components'
25 26  
26 27 const componentMap: Recordable<Component, ComponentName> = {
... ... @@ -45,7 +46,9 @@ const componentMap: Recordable&lt;Component, ComponentName&gt; = {
45 46 TreeSelect: ElTreeSelect,
46 47 RadioButton: ElRadioGroup,
47 48 InputPassword: InputPassword,
48   - Editor: Editor
  49 + Editor: Editor,
  50 + UploadImg: UploadImg,
  51 + UploadFile: UploadFile
49 52 }
50 53  
51 54 export { componentMap }
... ...
yudao-ui-admin-vue3/src/components/UploadFile/index.ts
1 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 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 26 <Dialog v-model="dialogVisible" title="预览" width="800" append-to-body>
24 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 35 import { ElUpload, UploadInstance, UploadProps, UploadRawFile, UploadUserFile } from 'element-plus'
33 36  
34 37 const message = useMessage() // 消息弹窗
35   -const emit = defineEmits(['input'])
  38 +const emit = defineEmits(['update:modelValue'])
36 39  
37 40 const props = defineProps({
38   - imgs: propTypes.oneOfType([String, Object, Array]),
  41 + modelValue: propTypes.oneOfType([String, Object, Array]),
39 42 title: propTypes.string.def('图片上传'),
40 43 updateUrl: propTypes.string.def(import.meta.env.VITE_UPLOAD_URL),
41 44 fileType: propTypes.array.def(['jpg', 'png', 'gif', 'jpeg']), // 文件类型, 例如['png', 'jpg', 'jpeg']
... ... @@ -44,6 +47,7 @@ const props = defineProps({
44 47 isShowTip: propTypes.bool.def(false) // 是否显示提示
45 48 })
46 49 // ========== 上传相关 ==========
  50 +const valueRef = ref(props.modelValue)
47 51 const uploadRef = ref<UploadInstance>()
48 52 const uploadList = ref<UploadUserFile[]>([])
49 53 const fileList = ref<UploadUserFile[]>([])
... ... @@ -55,15 +59,15 @@ const uploadHeaders = ref({
55 59 'tenant-id': getTenantId()
56 60 })
57 61 watch(
58   - () => props.imgs,
  62 + () => props.modelValue,
59 63 (val) => {
60 64 if (val) {
61 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 72 fileList.value = list.map((item) => {
69 73 if (typeof item === 'string') {
... ... @@ -84,6 +88,10 @@ watch(
84 88 )
85 89 // 文件上传之前判断
86 90 const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => {
  91 + if (fileList.value.length >= props.limit) {
  92 + message.error(`上传文件数量不能超过${props.limit}个!`)
  93 + return false
  94 + }
87 95 let fileExtension = ''
88 96 if (file.name.lastIndexOf('.') > -1) {
89 97 fileExtension = file.name.slice(file.name.lastIndexOf('.') + 1)
... ... @@ -111,14 +119,12 @@ const beforeUpload: UploadProps[&#39;beforeUpload&#39;] = (file: UploadRawFile) =&gt; {
111 119 // 文件上传成功
112 120 const handleFileSuccess: UploadProps['onSuccess'] = (res: any): void => {
113 121 message.success('上传成功')
114   - console.info(uploadList.value)
115   - console.info(fileList.value)
116 122 uploadList.value.push({ name: res.data, url: res.data })
117 123 if (uploadList.value.length == uploadNumber.value) {
118 124 fileList.value = fileList.value.concat(uploadList.value)
119 125 uploadList.value = []
120 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 140 const findex = fileList.value.map((f) => f.name).indexOf(file.name)
135 141 if (findex > -1) {
136 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 21 | 'TreeSelect'
22 22 | 'InputPassword'
23 23 | 'Editor'
  24 + | 'UploadImg'
  25 + | 'UploadFile'
24 26  
25 27 export type ColProps = {
26 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 41 cellRender: {
42 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 61 v-if="['create', 'update'].includes(actionType)"
62 62 :schema="allSchemas.formSchema"
63 63 :rules="rules"
64   - >
65   - <template #logo="form">
66   - <UploadImg :imgs="form['logo']" :limit="1" />
67   - </template>
68   - </Form>
  64 + />
69 65 <!-- 表单:详情 -->
70 66 <Descriptions
71 67 v-if="actionType === 'detail'"
72 68 :schema="allSchemas.detailSchema"
73   - :data="detailRef"
  69 + :data="detailData"
74 70 >
75 71 <template #accessTokenValiditySeconds="{ row }">
76 72 {{ row.accessTokenValiditySeconds + '秒' }}
... ... @@ -142,7 +138,6 @@ import { useMessage } from &#39;@/hooks/web/useMessage&#39;
142 138 import { useVxeGrid } from '@/hooks/web/useVxeGrid'
143 139 import { VxeGridInstance } from 'vxe-table'
144 140 import { FormExpose } from '@/components/Form'
145   -import { UploadImg } from '@/components/UploadFile'
146 141 // 业务相关的 import
147 142 import * as ClientApi from '@/api/system/oauth2/client'
148 143 import { rules, allSchemas } from './client.data'
... ... @@ -163,7 +158,7 @@ const dialogTitle = ref(&#39;edit&#39;) // 弹出层标题
163 158 const actionType = ref('') // 操作按钮的类型
164 159 const actionLoading = ref(false) // 按钮 Loading
165 160 const formRef = ref<FormExpose>() // 表单 Ref
166   -const detailRef = ref() // 详情 Ref
  161 +const detailData = ref() // 详情 Ref
167 162 // 设置标题
168 163 const setDialogTile = (type: string) => {
169 164 dialogTitle.value = t('action.' + type)
... ... @@ -188,7 +183,7 @@ const handleUpdate = async (rowId: number) =&gt; {
188 183 const handleDetail = async (rowId: number) => {
189 184 setDialogTile('detail')
190 185 const res = await ClientApi.getOAuth2ClientApi(rowId)
191   - detailRef.value = res
  186 + detailData.value = res
192 187 }
193 188  
194 189 // 删除操作
... ...