Commit 731e49d7b6fe37258b7c0d5894ce129b5a192c9c
1 parent
ba6a2957
feat: add upload component
Showing
8 changed files
with
234 additions
and
43 deletions
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<Component, ComponentName> = { |
| 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
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 '@/utils/auth' |
| 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['beforeUpload'] = (file: UploadRawFile) => { |
| 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) => { |
| 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
yudao-ui-admin-vue3/src/views/system/oauth2/client/client.data.ts
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 '@/hooks/web/useMessage' |
| 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('edit') // 弹出层标题 |
| 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) => { |
| 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 | // 删除操作 | ... | ... |