前端組件vue
最終效果
<template><div ><div class="file-list" v-if="existingFiles.length > 0"><div class="file-card" v-for="(file, index) in existingFiles" :key="file.id"><div class="file-preview"><i v-if="file.type === 'pdf'" class="el-icon-document pdf-preview"></i><i v-else-if="file.type === 'doc' || file.type === 'docx'" class="el-icon-document doc-preview"></i><i v-else-if="file.type === 'image'" class="el-icon-picture-outline"></i><i v-else class="el-icon-document other-preview"></i></div><div class="file-name">{{ file.originalFileName }}</div><div class="file-actions"><el-button type="primary" icon="el-icon-view" size="mini" circle @click="handlePreview(file)"></el-button><el-button type="success" icon="el-icon-download" size="mini" v-if="showDownLoadBut" circle @click="downloadFile(file)"></el-button><el-button type="danger" icon="el-icon-delete" size="mini" v-if="showDeleteBut" circle @click="handleDelete(file, index)"></el-button></div></div></div><el-alertv-elsetitle="當前尚未上傳任何合同"type="warning"show-iconstyle="margin: 20px 0;"></el-alert><div class="upload-demo" v-if="existsUploadBut"><el-uploadref="upload":action="uploadUrl":headers="headers"multiple:on-success="handleUploadSuccess":show-file-list="false"accept=".pdf,.doc,.docx,.jpg,.jpeg,.png"><el-button size="medium" type="primary" icon="el-icon-plus">添加合同文件</el-button><div slot="tip" class="el-upload__tip" style="margin-top: 10px;">支持上傳PDF、Word文檔和圖片文件,單個文件不超過10MB</div></el-upload></div><!-- 文件預覽彈窗 --><el-dialog:title="previewTitle":visible.sync="previewVisible"width="80%"top="5vh"><div class="preview-container"><img v-if="previewType === 'image'" :src="previewUrl"><iframev-else-if="previewType === 'pdf'":src="previewUrl"class="preview-iframe"></iframe><div v-else><p>不支持在線預覽,請下載后查看</p><el-button type="primary" icon="el-icon-download" @click="downloadFile(previewFile)">下載文件</el-button></div></div></el-dialog></div>
</template>
<script>export default {name: "FileUploadPreview",props:{// 上傳文件目錄fileDirectory: {type: String,require: true,default: ''},// 文件列表fileList:{type:Array,default:()=>[]},// 上傳按鈕existsUploadBut:{type: Boolean,default: false},// 下載按鈕是否顯示showDownLoadBut:{type: Boolean,default: false},// 刪除按鈕showDeleteBut:{type: Boolean,default: false}},data() {return {previewVisible: false,existingFiles: this.fileList,previewUrl: '',previewType: '',previewTitle: '查看附件',previewFile: null,// 請求頭 - 包含認證Tokenheaders: {Authorization: 'Bearer ' + this.$store.getters.token},uploadUrl: process.env.VUE_APP_BASE_API + "/common/upload?fileDirectory="+this.fileDirectory,imageDialogVisible: false, // 圖片彈窗可見性previewVisible: false, // 預覽彈窗可見性previewImageUrl: '', // 預覽圖片URLtempImageList: [], // 臨時圖片列表};},created() {},methods: {// 移除待上傳文件removeNewFile(index) {this.existingFiles.splice(index, 1);},// 預覽文件handlePreview(file) {this.previewFile = file;this.previewTitle = `預覽文件 - ${file.originalFileName}`;this.previewUrl = file.url==null?file.filePath:"";this.previewType = this.getFileType(file.originalFileName);this.previewVisible = true;},// 刪除文件handleDelete(file, index) {this.$confirm(`確定要刪除合同 "${file.originalFileName}" 嗎?`, '提示', {confirmButtonText: '確定',cancelButtonText: '取消',type: 'warning'}).then(() => {// 模擬API刪除請求setTimeout(() => {this.existingFiles.splice(index, 1);this.$message.success(`合同 "${file.originalFileName}" 已刪除`);}, 500);}).catch(() => {});},// 獲取文件類型getFileType(filename) {const ext = filename.split('.').pop().toLowerCase();if (['jpg','jpeg','png','gif'].includes(ext)) return 'image';if (ext === 'pdf') return 'pdf';if (['doc','docx'].includes(ext)) return 'doc';return 'other';},// 格式化文件大小formatFileSize(bytes) {if (bytes === 0) return '0 Bytes';const k = 1024;const sizes = ['Bytes', 'KB', 'MB', 'GB'];const i = Math.floor(Math.log(bytes) / Math.log(k));return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];},// 下載文件downloadFile(file) {this.$message.info(`開始下載: ${file.originalFileName}`);// 實際項目中這里應該是文件下載邏輯this.$download.systemDownLoadFile(file.filePath,file.originalFileName);},// 打開圖片彈窗openImageDialog(row) {this.imageDialogVisible = true;// 加載已有圖片 (實際項目中從后端獲取)getWarehouseFiles(row.uuid).then(resp=>{this.tempImageList = resp.data;});},// 圖片上傳前校驗beforeImageUpload(file) {const isImage = file.type.startsWith('image/');const isPdf = file.type === 'application/pdf';const isLt10M = file.size / 1024 / 1024 < 10;if (!(isImage || isPdf)) {this.$message.error('只能上傳圖片或PDF文件!');return false;}if (!isLt10M) {this.$message.error('文件大小不能超過10MB!');return false;}return true;},// 圖片上傳成功處理handleUploadSuccess(response, file, fileList) {// 實際項目中根據接口返回調整// this.newFiles = fileList.map(f => ({// originalFileName: f.response?.originalFilename || f.originalFileName,// newFileName: f.response?.newFileName || f.newFileName,// filePath: f.response?.filePath || f.filePath,// fileSize: f.response?.size || f.fileSize,// fileShowSize: f.response?.showSize || f.fileShowSize,// fileType: f.response?.fileType || f.fileType,// warehouseManagerUuid: f.response?.warehouseManagerUuid || f.warehouseManagerUuid,// url: f.response?.url || f.url,// }));debuggerif(response.code==200){var tempData={originalFileName: response.originalFilename,newFileName:response.newFileName,filePath:response.filePath,fileSize:response.size,fileShowSize: response.showSize,fileType:response.fileType,name:response.originalFilename,url:response.url}this.existingFiles.push(tempData);}},// 預覽圖片handlePictureCardPreview(file) {this.previewImageUrl = file.url;this.previewVisible = true;},// 移除圖片handleRemove(file, fileList) {this.tempImageList = fileList;},}
};
</script>
<style scoped>body {font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", Arial, sans-serif;background-color: #f5f7fa;color: #333;padding: 20px;}.container {max-width: 1200px;margin: 0 auto;background: white;border-radius: 8px;box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);padding: 20px;}.header {display: flex;justify-content: space-between;align-items: center;padding: 15px 0;border-bottom: 1px solid #ebeef5;margin-bottom: 20px;}.header h1 {color: #409EFF;font-size: 24px;}.sub-title {color: #909399;margin: 10px 0 20px;}.contract-actions {display: flex;gap: 10px;margin-bottom: 20px;}.file-card {position: relative;border-radius: 6px;overflow: hidden;transition: all 0.3s;border: 1px solid #ebeef5;}.file-card:hover {box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);}.file-name {padding: 8px;font-size: 12px;text-align: center;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;background: #f8f8f8;}.file-actions {position: absolute;top: 5px;right: 5px;display: flex;gap: 5px;opacity: 0;transition: opacity 0.3s;}.file-card:hover .file-actions {opacity: 1;}.file-preview {display: flex;align-items: center;justify-content: center;height: 100px;background: #f5f7fa;}.file-preview i {font-size: 40px;color: #409EFF;}.pdf-preview {color: #F56C6C;}.doc-preview {color: #409EFF;}.other-preview {color: #909399;}.file-list {display: grid;grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));gap: 15px;margin-top: 20px;}.preview-container {width: 100%;height: 70vh;}.preview-container img {max-width: 100%;max-height: 100%;display: block;margin: 0 auto;}.preview-iframe {width: 100%;height: 100%;border: none;}.footer {margin-top: 20px;text-align: center;color: #909399;font-size: 14px;padding: 10px;border-top: 1px solid #ebeef5;}.upload-demo {margin-top: 20px;}.demo-table {margin-top: 20px;}.status-badge {display: inline-block;padding: 2px 8px;border-radius: 4px;font-size: 12px;}.status-success {background: #f0f9eb;color: #67c23a;}.status-warning {background: #fdf6ec;color: #e6a23c;}
</style>
使用瀏覽器確認保存路徑
systemDownLoadFile(filePath,fileName) {var url = baseURL + "/common/systemDownLoadFile?filePath=" + encodeURIComponent(filePath)+"&fileName="+encodeURIComponent(fileName);axios({method: 'get',url: url,responseType: 'blob',headers: { 'Authorization': 'Bearer ' + getToken() }}).then((res) => {const isBlob = blobValidate(res.data);if (isBlob) {const blob = new Blob([res.data])this.saveAs(blob, decodeURIComponent(res.headers['download-filename']))} else {this.printErrMsg(res.data);}})},
后端代碼
/*** 下載文件** @param filePath 文件路徑* @param fileName 文件原始名* @param response* @param request*/@GetMapping("/systemDownLoadFile")public void systemDownLoadFile(@RequestParam("filePath") String filePath, @RequestParam("fileName") String fileName, HttpServletResponse response,HttpServletRequest request) {try {if (!FileUtils.checkAllowDownload(fileName)) {throw new Exception(StringUtils.format("文件名稱({})非法,不允許下載。 ", fileName));}String configByKey = iSysConfigService.selectConfigByKey(SysConfigConstants.UPLOAD_ROOT);
// String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1);String fileAllPath = configByKey + filePath;// 上傳文件路徑if (StringUtils.isEmpty(configByKey)) {log.info("服務器文件上傳地址不能為空,請檢查參數設置的 {} 是否配置值", SysConfigConstants.UPLOAD_ROOT);return;}response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);FileUtils.setAttachmentResponseHeader(response, fileName);FileUtils.writeBytes(fileAllPath, response.getOutputStream());} catch (Exception e) {log.error("下載文件失敗", e);}}
下載resources目錄下的文件
@GetMapping("/downloadTemplates")ResponseEntity<Resource> downloadFile(@RequestParam("fileName") String fileName) {// 驗證文件名安全(防止路徑遍歷攻擊)if (!isValidFileName(fileName)) {throw new FileDownloadException("文件名不合法", HttpStatus.BAD_REQUEST);}// 從resources目錄加載文件Resource resource = new ClassPathResource("templates/" + fileName);// 檢查文件是否存在if (!resource.exists()) {throw new FileDownloadException("文件不存在", HttpStatus.NOT_FOUND);}try {// 設置響應頭HttpHeaders headers = new HttpHeaders();headers.add(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=\"" + encodeFilename(fileName) + "\"");return ResponseEntity.ok().headers(headers).contentLength(resource.contentLength()).contentType(MediaType.APPLICATION_OCTET_STREAM).body(resource);} catch (IOException e) {return ResponseEntity.internalServerError().build();}}/*** 安全驗證文件名(防止路徑遍歷攻擊)** @param fileName* @return*/private boolean isValidFileName(String fileName) {return fileName != null &&!fileName.contains("..") &&!fileName.contains("/") &&!fileName.contains("\\");}/*** 處理中文文件名亂碼問題** @param fileName* @return*/private String encodeFilename(String fileName) {return new String(fileName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);}