前端组件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-alert
v-else
title="当前尚未上传任何合同"
type="warning"
show-icon
style="margin: 20px 0;">
</el-alert>
<div class="upload-demo" v-if="existsUploadBut">
<el-upload
ref="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">
<iframe
v-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,
// 请求头 - 包含认证Token
headers: {
Authorization: 'Bearer ' + this.$store.getters.token
},
uploadUrl: process.env.VUE_APP_BASE_API + "/common/upload?fileDirectory="+this.fileDirectory,
imageDialogVisible: false, // 图片弹窗可见性
previewVisible: false, // 预览弹窗可见性
previewImageUrl: '', // 预览图片URL
tempImageList: [], // 临时图片列表
};
},
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,
// }));
debugger
if(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);
}