mirror of
				https://github.com/continew-org/continew-admin.git
				synced 2025-10-31 00:57:13 +08:00 
			
		
		
		
	feat: 文件管理适配后端文件上传、单个删除、批量删除 API
This commit is contained in:
		| @@ -76,18 +76,7 @@ public class FileRecorderImpl implements FileRecorder { | |||||||
|         if (null == file) { |         if (null == file) { | ||||||
|             return null; |             return null; | ||||||
|         } |         } | ||||||
|         FileInfo fileInfo = new FileInfo(); |         return file.toFileInfo(storageMapper.lambdaQuery().eq(StorageDO::getId, file.getStorageId()).one().getCode()); | ||||||
|         String extension = file.getExtension(); |  | ||||||
|         fileInfo.setOriginalFilename( |  | ||||||
|             StrUtil.isNotBlank(extension) ? file.getName() + StringConstants.DOT + extension : file.getName()); |  | ||||||
|         fileInfo.setSize(file.getSize()); |  | ||||||
|         fileInfo.setUrl(file.getUrl()); |  | ||||||
|         fileInfo.setExt(extension); |  | ||||||
|         fileInfo.setBasePath(StringConstants.EMPTY); |  | ||||||
|         fileInfo.setPath(StringConstants.EMPTY); |  | ||||||
|         fileInfo.setFilename(StrUtil.subAfter(url, StringConstants.SLASH, true)); |  | ||||||
|         fileInfo.setPlatform(storageMapper.lambdaQuery().eq(StorageDO::getId, file.getStorageId()).one().getCode()); |  | ||||||
|         return fileInfo; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|   | |||||||
| @@ -20,9 +20,15 @@ import java.io.Serial; | |||||||
|  |  | ||||||
| import lombok.Data; | import lombok.Data; | ||||||
|  |  | ||||||
|  | import org.dromara.x.file.storage.core.FileInfo; | ||||||
|  |  | ||||||
| import com.baomidou.mybatisplus.annotation.*; | import com.baomidou.mybatisplus.annotation.*; | ||||||
|  |  | ||||||
|  | import cn.hutool.core.util.StrUtil; | ||||||
|  |  | ||||||
| import top.charles7c.continew.admin.system.enums.FileTypeEnum; | import top.charles7c.continew.admin.system.enums.FileTypeEnum; | ||||||
|  | import top.charles7c.continew.starter.core.constant.StringConstants; | ||||||
|  | import top.charles7c.continew.starter.core.util.URLUtils; | ||||||
| import top.charles7c.continew.starter.extension.crud.base.BaseDO; | import top.charles7c.continew.starter.extension.crud.base.BaseDO; | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -70,4 +76,26 @@ public class FileDO extends BaseDO { | |||||||
|      * 存储库 ID |      * 存储库 ID | ||||||
|      */ |      */ | ||||||
|     private Long storageId; |     private Long storageId; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 转换为 X-File-Storage 文件信息对象 | ||||||
|  |      * | ||||||
|  |      * @param storageCode | ||||||
|  |      *            存储库编码 | ||||||
|  |      * @return X-File-Storage 文件信息对象 | ||||||
|  |      */ | ||||||
|  |     public FileInfo toFileInfo(String storageCode) { | ||||||
|  |         FileInfo fileInfo = new FileInfo(); | ||||||
|  |         fileInfo.setOriginalFilename( | ||||||
|  |             StrUtil.isNotBlank(this.extension) ? this.name + StringConstants.DOT + this.extension : this.name); | ||||||
|  |         fileInfo.setSize(this.size); | ||||||
|  |         fileInfo.setUrl(this.url); | ||||||
|  |         fileInfo.setExt(this.extension); | ||||||
|  |         fileInfo.setBasePath(StringConstants.EMPTY); | ||||||
|  |         fileInfo.setPath(StringConstants.EMPTY); | ||||||
|  |         fileInfo.setFilename( | ||||||
|  |             URLUtils.isHttpUrl(this.url) ? StrUtil.subAfter(this.url, StringConstants.SLASH, true) : this.url); | ||||||
|  |         fileInfo.setPlatform(storageCode); | ||||||
|  |         return fileInfo; | ||||||
|  |     } | ||||||
| } | } | ||||||
| @@ -18,22 +18,34 @@ package top.charles7c.continew.admin.system.model.req; | |||||||
|  |  | ||||||
| import java.io.Serial; | import java.io.Serial; | ||||||
|  |  | ||||||
|  | import jakarta.validation.constraints.NotBlank; | ||||||
|  |  | ||||||
| import lombok.Data; | import lombok.Data; | ||||||
|  |  | ||||||
| import io.swagger.v3.oas.annotations.media.Schema; | import io.swagger.v3.oas.annotations.media.Schema; | ||||||
|  |  | ||||||
|  | import org.hibernate.validator.constraints.Length; | ||||||
|  |  | ||||||
| import top.charles7c.continew.starter.extension.crud.base.BaseReq; | import top.charles7c.continew.starter.extension.crud.base.BaseReq; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * 创建或修改文件信息 |  * 修改文件信息 | ||||||
|  * |  * | ||||||
|  * @author Charles7c |  * @author Charles7c | ||||||
|  * @since 2023/12/23 10:38 |  * @since 2023/12/23 10:38 | ||||||
|  */ |  */ | ||||||
| @Data | @Data | ||||||
| @Schema(description = "创建或修改文件信息") | @Schema(description = "修改文件信息") | ||||||
| public class FileReq extends BaseReq { | public class FileReq extends BaseReq { | ||||||
|  |  | ||||||
|     @Serial |     @Serial | ||||||
|     private static final long serialVersionUID = 1L; |     private static final long serialVersionUID = 1L; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 名称 | ||||||
|  |      */ | ||||||
|  |     @Schema(description = "名称", example = "test123") | ||||||
|  |     @NotBlank(message = "文件名称不能为空") | ||||||
|  |     @Length(max = 255, message = "文件名称长度不能超过 {max} 个字符") | ||||||
|  |     private String name; | ||||||
| } | } | ||||||
| @@ -17,6 +17,8 @@ | |||||||
| package top.charles7c.continew.admin.system.service.impl; | package top.charles7c.continew.admin.system.service.impl; | ||||||
|  |  | ||||||
| import java.util.List; | import java.util.List; | ||||||
|  | import java.util.Map; | ||||||
|  | import java.util.stream.Collectors; | ||||||
|  |  | ||||||
| import jakarta.annotation.Resource; | import jakarta.annotation.Resource; | ||||||
|  |  | ||||||
| @@ -62,6 +64,20 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes | |||||||
|     private StorageService storageService; |     private StorageService storageService; | ||||||
|     private final FileStorageService fileStorageService; |     private final FileStorageService fileStorageService; | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void delete(List<Long> ids) { | ||||||
|  |         List<FileDO> fileList = baseMapper.lambdaQuery().in(FileDO::getId, ids).list(); | ||||||
|  |         Map<Long, List<FileDO>> fileListGroup = fileList.stream().collect(Collectors.groupingBy(FileDO::getStorageId)); | ||||||
|  |         for (Map.Entry<Long, List<FileDO>> entry : fileListGroup.entrySet()) { | ||||||
|  |             StorageDetailResp storage = storageService.get(entry.getKey()); | ||||||
|  |             for (FileDO file : entry.getValue()) { | ||||||
|  |                 FileInfo fileInfo = file.toFileInfo(storage.getCode()); | ||||||
|  |                 fileStorageService.delete(fileInfo); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         super.delete(ids); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public FileInfo upload(MultipartFile file, String storageCode) { |     public FileInfo upload(MultipartFile file, String storageCode) { | ||||||
|         StorageDO storage; |         StorageDO storage; | ||||||
|   | |||||||
| @@ -47,7 +47,11 @@ export function get(id: string) { | |||||||
|   return axios.get<FileItem>(`${BASE_URL}/${id}`); |   return axios.get<FileItem>(`${BASE_URL}/${id}`); | ||||||
| } | } | ||||||
|  |  | ||||||
| export function update(req: FileItem, id: string) { | export interface FileItemUpdate { | ||||||
|  |   name: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function update(req: FileItemUpdate, id: string) { | ||||||
|   return axios.put(`${BASE_URL}/${id}`, req); |   return axios.put(`${BASE_URL}/${id}`, req); | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -10,7 +10,6 @@ import type { RouteRecordNormalized } from 'vue-router'; | |||||||
| import defaultSettings from '@/config/settings.json'; | import defaultSettings from '@/config/settings.json'; | ||||||
| import { listRoute } from '@/api/auth'; | import { listRoute } from '@/api/auth'; | ||||||
| import { listOption } from '@/api/common'; | import { listOption } from '@/api/common'; | ||||||
| import getFile from '@/utils/file'; |  | ||||||
| import { AppState, Config } from './types'; | import { AppState, Config } from './types'; | ||||||
|  |  | ||||||
| const recursionMenu = ( | const recursionMenu = ( | ||||||
| @@ -134,7 +133,7 @@ const useAppStore = defineStore('app', { | |||||||
|           .querySelector('link[rel="shortcut icon"]') |           .querySelector('link[rel="shortcut icon"]') | ||||||
|           ?.setAttribute( |           ?.setAttribute( | ||||||
|             'href', |             'href', | ||||||
|             getFile(resMap.get('site_favicon')) || |             resMap.get('site_favicon') || | ||||||
|               'https://cnadmin.charles7c.top/favicon.ico', |               'https://cnadmin.charles7c.top/favicon.ico', | ||||||
|           ); |           ); | ||||||
|       }); |       }); | ||||||
| @@ -152,8 +151,7 @@ const useAppStore = defineStore('app', { | |||||||
|         .querySelector('link[rel="shortcut icon"]') |         .querySelector('link[rel="shortcut icon"]') | ||||||
|         ?.setAttribute( |         ?.setAttribute( | ||||||
|           'href', |           'href', | ||||||
|           getFile(config.site_favicon) || |           config.site_favicon || 'https://cnadmin.charles7c.top/favicon.ico', | ||||||
|             'https://cnadmin.charles7c.top/favicon.ico', |  | ||||||
|         ); |         ); | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|   | |||||||
| @@ -1,13 +0,0 @@ | |||||||
| export default function getFile(file: string | undefined) { |  | ||||||
|   if (file) { |  | ||||||
|     const baseUrl = import.meta.env.VITE_API_BASE_URL; |  | ||||||
|     if ( |  | ||||||
|       !file.startsWith('http://') && |  | ||||||
|       !file.startsWith('https://') && |  | ||||||
|       !file.startsWith('blob:') |  | ||||||
|     ) { |  | ||||||
|       return `${baseUrl}/file/${file}`; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   return file; |  | ||||||
| } |  | ||||||
| @@ -20,7 +20,7 @@ | |||||||
|           label="文件名称" |           label="文件名称" | ||||||
|           :rules="[{ required: true, message: '请输入文件名称' }]" |           :rules="[{ required: true, message: '请输入文件名称' }]" | ||||||
|         > |         > | ||||||
|           <a-input v-model="form.name" placeholder="请输入" allow-clear /> |           <a-input v-model="form.name" placeholder="请输入文件名称" allow-clear /> | ||||||
|         </a-form-item> |         </a-form-item> | ||||||
|       </a-form> |       </a-form> | ||||||
|     </a-row> |     </a-row> | ||||||
| @@ -28,16 +28,16 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|  |   import { getCurrentInstance, onMounted, reactive, ref } from 'vue'; | ||||||
|   import type { FormInstance, Modal } from '@arco-design/web-vue'; |   import type { FormInstance, Modal } from '@arco-design/web-vue'; | ||||||
|   import type { FileItem } from '@/api/system/file'; |   import { FileItem, update } from '@/api/system/file'; | ||||||
|   import { onMounted, reactive, ref } from 'vue'; |  | ||||||
|  |  | ||||||
|   interface Props { |   interface Props { | ||||||
|     fileInfo: FileItem; |     fileInfo: FileItem; | ||||||
|     onClose: () => void; |     onClose: () => void; | ||||||
|   } |   } | ||||||
|   const props = withDefaults(defineProps<Props>(), {}); |   const props = withDefaults(defineProps<Props>(), {}); | ||||||
|  |   const { proxy } = getCurrentInstance() as any; | ||||||
|   const visible = ref(false); |   const visible = ref(false); | ||||||
|   type Form = { name: string }; |   type Form = { name: string }; | ||||||
|   const form: Form = reactive({ |   const form: Form = reactive({ | ||||||
| @@ -54,20 +54,15 @@ | |||||||
|     props.onClose(); |     props.onClose(); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   // 模拟接口 |  | ||||||
|   const saveApi = (): Promise<boolean> => { |  | ||||||
|     return new Promise((resolve) => { |  | ||||||
|       setTimeout(() => { |  | ||||||
|         resolve(true); |  | ||||||
|       }, 2000); |  | ||||||
|     }); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   const FormRef = ref<FormInstance | null>(null); |   const FormRef = ref<FormInstance | null>(null); | ||||||
|   const save: InstanceType<typeof Modal>['onBeforeOk'] = async () => { |   const save: InstanceType<typeof Modal>['onBeforeOk'] = async () => { | ||||||
|     const flag = await FormRef.value?.validate(); |     const flag = await FormRef.value?.validate(); | ||||||
|     if (flag) return false; |     if (flag) return false; | ||||||
|     return saveApi(); |     const res = await update({ name: form.name }, props.fileInfo.id); | ||||||
|  |     // eslint-disable-next-line vue/no-mutating-props | ||||||
|  |     props.fileInfo.name = form.name; | ||||||
|  |     proxy.$message.success(res.msg); | ||||||
|  |     return true; | ||||||
|   }; |   }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| <template> | <template> | ||||||
|   <img v-if="isImage" class="img" :src="props.data.url || ''" alt="" /> |   <img v-if="isImage" class="img" :src="props.data.url || ''" alt="" /> | ||||||
|   <svg-icon v-else size="100%" :icon-class="getFileImg" /> |   <svg-icon v-else :icon-class="getFileImg" style="height: 100%; width: 100%" /> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|   | |||||||
| @@ -37,11 +37,11 @@ | |||||||
|         </a-table-column> |         </a-table-column> | ||||||
|         <a-table-column |         <a-table-column | ||||||
|           title="扩展名" |           title="扩展名" | ||||||
|           data-index="extendName" |           data-index="extension" | ||||||
|           :width="100" |           :width="100" | ||||||
|         ></a-table-column> |         ></a-table-column> | ||||||
|         <a-table-column |         <a-table-column | ||||||
|           title="更改时间" |           title="修改时间" | ||||||
|           data-index="updateTime" |           data-index="updateTime" | ||||||
|           :width="200" |           :width="200" | ||||||
|         ></a-table-column> |         ></a-table-column> | ||||||
|   | |||||||
| @@ -5,10 +5,14 @@ | |||||||
|       <a-space wrap> |       <a-space wrap> | ||||||
|         <a-form ref="queryRef" :model="queryParams" layout="inline"> |         <a-form ref="queryRef" :model="queryParams" layout="inline"> | ||||||
|           <a-form-item hide-label> |           <a-form-item hide-label> | ||||||
|  |             <a-upload :show-file-list="false" :custom-request="handleUpload"> | ||||||
|  |               <template #upload-button> | ||||||
|                 <a-button type="primary" shape="round"> |                 <a-button type="primary" shape="round"> | ||||||
|                   <template #icon><icon-upload /></template> |                   <template #icon><icon-upload /></template> | ||||||
|                   <template #default>上传</template> |                   <template #default>上传</template> | ||||||
|                 </a-button> |                 </a-button> | ||||||
|  |               </template> | ||||||
|  |             </a-upload> | ||||||
|           </a-form-item> |           </a-form-item> | ||||||
|           <a-form-item field="name" hide-label> |           <a-form-item field="name" hide-label> | ||||||
|             <a-input |             <a-input | ||||||
| @@ -87,12 +91,13 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|   import { Message, Modal } from '@arco-design/web-vue'; |   import { Message, Modal, RequestOption } from '@arco-design/web-vue'; | ||||||
|   import { api as viewerApi } from 'v-viewer'; |   import { api as viewerApi } from 'v-viewer'; | ||||||
|   import { imageTypeList } from '@/constant/file'; |   import { imageTypeList } from '@/constant/file'; | ||||||
|   import { useFileStore } from '@/store/modules/file'; |   import { useFileStore } from '@/store/modules/file'; | ||||||
|   import type { ListParam, FileItem } from '@/api/system/file'; |   import type { ListParam, FileItem } from '@/api/system/file'; | ||||||
|   import { list } from '@/api/system/file'; |   import { list, del } from '@/api/system/file'; | ||||||
|  |   import { upload } from '@/api/common'; | ||||||
|   import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router'; |   import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router'; | ||||||
|   import { getCurrentInstance, onMounted, reactive, ref, toRefs } from 'vue'; |   import { getCurrentInstance, onMounted, reactive, ref, toRefs } from 'vue'; | ||||||
|   import FileGrid from './FileGrid.vue'; |   import FileGrid from './FileGrid.vue'; | ||||||
| @@ -178,12 +183,17 @@ | |||||||
|   }; |   }; | ||||||
|   // 鼠标右键 |   // 鼠标右键 | ||||||
|   const handleRightMenuClick = (mode: string, fileInfo: FileItem) => { |   const handleRightMenuClick = (mode: string, fileInfo: FileItem) => { | ||||||
|     Message.success(`点击了${mode}`); |  | ||||||
|     if (mode === 'delete') { |     if (mode === 'delete') { | ||||||
|       Modal.warning({ |       Modal.warning({ | ||||||
|         title: '提示', |         title: '提示', | ||||||
|         content: '是否删除该文件?', |         content: `是否确定删除文件 [${fileInfo.name}]?`, | ||||||
|         hideCancel: false, |         hideCancel: false, | ||||||
|  |         onOk: () => { | ||||||
|  |           del(fileInfo.id).then((res) => { | ||||||
|  |             proxy.$message.success(res.msg); | ||||||
|  |             getList(); | ||||||
|  |           }); | ||||||
|  |         }, | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|     if (mode === 'rename') { |     if (mode === 'rename') { | ||||||
| @@ -198,9 +208,50 @@ | |||||||
|   const handleMulDelete = () => { |   const handleMulDelete = () => { | ||||||
|     Modal.warning({ |     Modal.warning({ | ||||||
|       title: '提示', |       title: '提示', | ||||||
|       content: '是否确认删除?', |       content: `是否确定删除所选的${fileStore.selectedFileIds.length}个文件?`, | ||||||
|       hideCancel: false, |       hideCancel: false, | ||||||
|  |       onOk: () => { | ||||||
|  |         del(fileStore.selectedFileIds).then((res) => { | ||||||
|  |           proxy.$message.success(res.msg); | ||||||
|  |           getList(); | ||||||
|         }); |         }); | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 上传 | ||||||
|  |    * | ||||||
|  |    * @param options / | ||||||
|  |    */ | ||||||
|  |   const handleUpload = (options: RequestOption) => { | ||||||
|  |     const controller = new AbortController(); | ||||||
|  |     (async function requestWrap() { | ||||||
|  |       const { | ||||||
|  |         onProgress, | ||||||
|  |         onError, | ||||||
|  |         onSuccess, | ||||||
|  |         fileItem, | ||||||
|  |         name = 'file', | ||||||
|  |       } = options; | ||||||
|  |       onProgress(20); | ||||||
|  |       const formData = new FormData(); | ||||||
|  |       formData.append(name as string, fileItem.file as Blob); | ||||||
|  |       upload(formData) | ||||||
|  |         .then((res) => { | ||||||
|  |           onSuccess(res); | ||||||
|  |           getList(); | ||||||
|  |           proxy.$message.success(res.msg); | ||||||
|  |         }) | ||||||
|  |         .catch((error) => { | ||||||
|  |           onError(error); | ||||||
|  |         }); | ||||||
|  |     })(); | ||||||
|  |     return { | ||||||
|  |       abort() { | ||||||
|  |         controller.abort(); | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user