<template>
|
<div class="lims-attachment">
|
<div v-if="inline" class="attachment-inline">
|
<div class="attachment-header">
|
<div class="el-upload__tip">
|
{{ readonly ? '仅可预览、下载附件' : ((currentId || deferUpload) ? tip : emptyTip) }}
|
</div>
|
<el-upload v-if="!readonly" :action="uploadAction" :headers="uploadHeader" multiple
|
:show-file-list="false" :disabled="!canUpload" :auto-upload="!deferUpload"
|
:on-change="handleFileChange" :on-success="handleUploadSuccess"
|
:on-error="handleUploadError">
|
<el-button size="small" type="primary" :disabled="!canUpload">上传附件</el-button>
|
</el-upload>
|
</div>
|
<el-table :data="displayList" border size="small" max-height="180" style="margin-top:8px">
|
<el-table-column label="文件名称" prop="fileName" min-width="220" show-overflow-tooltip />
|
<el-table-column label="上传时间" prop="createTime" width="160" />
|
<el-table-column label="操作" width="170">
|
<template #default="scope">
|
<template v-if="scope.row.pending">
|
<el-button link type="danger" @click="removePending(scope.row)">删除</el-button>
|
</template>
|
<template v-else>
|
<el-button link type="primary" @click="previewFile(scope.row)">预览</el-button>
|
<el-button link type="primary" @click="downloadFile(scope.row)">下载</el-button>
|
<el-button v-if="!readonly" link type="danger" @click="removeFile(scope.row)">删除</el-button>
|
</template>
|
</template>
|
</el-table-column>
|
</el-table>
|
</div>
|
<el-dialog v-else :title="dialogTitle" v-model="visible" width="760px" append-to-body @close="$emit('closed')">
|
<div class="attachment-header">
|
<div class="el-upload__tip">{{ readonly ? '仅可预览、下载附件' : tip }}</div>
|
<el-upload v-if="!readonly" :action="uploadAction" :headers="uploadHeader" multiple
|
:show-file-list="false" :on-success="handleUploadSuccess" :on-error="handleUploadError">
|
<el-button size="small" type="primary">上传附件</el-button>
|
</el-upload>
|
</div>
|
<el-table :data="attachmentList" border size="small" style="margin-top:12px">
|
<el-table-column label="文件名称" prop="fileName" min-width="280" show-overflow-tooltip />
|
<el-table-column label="上传时间" prop="createTime" width="170" />
|
<el-table-column label="操作" width="180">
|
<template #default="scope">
|
<el-button link type="primary" @click="previewFile(scope.row)">预览</el-button>
|
<el-button link type="primary" @click="downloadFile(scope.row)">下载</el-button>
|
<el-button v-if="!readonly" link type="danger" @click="removeFile(scope.row)">删除</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
<template #footer>
|
<el-button @click="visible = false">关 闭</el-button>
|
</template>
|
</el-dialog>
|
<el-dialog title="预览附件" v-model="previewVisible" fullscreen append-to-body>
|
<file-preview v-if="previewVisible" :file-url="previewUrl" :current-file="previewRow" />
|
</el-dialog>
|
</div>
|
</template>
|
|
<script>
|
import request from '@/utils/request'
|
import filePreview from '@/components/Preview/filePreview.vue'
|
|
/**
|
* 通用附件管理组件(上传 / 预览 / 下载 / 删除)
|
*
|
* 用法:页面操作列放三个入口,分别调用本组件的方法:
|
* 上传附件 -> this.$refs.xxx.open(row) 打开附件弹窗(弹窗内可上传)
|
* 下载附件 -> this.$refs.xxx.downloadAttachments(row) 只有一个附件直接下载,多个打开弹窗
|
* 预览附件 -> this.$refs.xxx.previewAttachments(row) 只有一个附件直接预览,多个打开弹窗
|
*
|
* 后端接口需按统一约定提供(参照 /supplierManagement 的附件接口):
|
* 上传:POST {uploadUrl}?{idParam}=主表ID 表单参数 file
|
* 列表:GET {selectUrl}?{idParam}=主表ID 返回 [{id, fileName, fileUrl, createTime, ...}]
|
* 删除:DELETE {deleteUrl}?id=附件ID
|
*/
|
export default {
|
name: 'LimsAttachment',
|
components: {
|
filePreview
|
},
|
emits: ['closed'],
|
props: {
|
// 上传接口地址,如 /supplierManagement/uploadAttachment
|
uploadUrl: {
|
type: String,
|
required: true
|
},
|
// 查询附件列表接口地址
|
selectUrl: {
|
type: String,
|
required: true
|
},
|
// 删除附件接口地址
|
deleteUrl: {
|
type: String,
|
required: true
|
},
|
// 关联主表ID在接口里的参数名,如 supplierManagementId
|
idParam: {
|
type: String,
|
default: 'id'
|
},
|
// 关联主表ID在行数据里的字段名,不传时默认与 idParam 相同
|
idField: {
|
type: String,
|
default: ''
|
},
|
// 弹窗标题上要显示名称的行字段,如 supplierName
|
nameField: {
|
type: String,
|
default: ''
|
},
|
// 弹窗标题前缀
|
title: {
|
type: String,
|
default: '附件'
|
},
|
// 弹窗左上角提示文字
|
tip: {
|
type: String,
|
default: '支持任意格式文件,单个文件不超过200MB'
|
},
|
// 只读模式:仅预览/下载,不显示上传和删除
|
readonly: {
|
type: Boolean,
|
default: false
|
},
|
// 内嵌模式:直接渲染上传区和附件列表,不弹管理弹窗
|
inline: {
|
type: Boolean,
|
default: false
|
},
|
// 内嵌模式关联主表ID,变化时自动加载附件
|
mainId: {
|
type: [String, Number],
|
default: ''
|
},
|
// 内嵌模式未关联主表ID时的提示
|
emptyTip: {
|
type: String,
|
default: '请先选择数据后再上传附件'
|
},
|
// 内嵌模式:没有主表ID时也允许选择文件,先暂存在页面,提交时再统一上传
|
deferUpload: {
|
type: Boolean,
|
default: false
|
}
|
},
|
watch: {
|
mainId: {
|
immediate: true,
|
handler(val) {
|
if (!this.inline) {
|
return
|
}
|
const field = this.idField || this.idParam
|
this.currentRow = { [field]: val }
|
this.attachmentList = []
|
if (val !== null && val !== undefined && val !== '') {
|
this.loadList()
|
}
|
}
|
}
|
},
|
data() {
|
return {
|
visible: false,
|
attachmentList: [],
|
pendingFiles: [],
|
currentRow: {},
|
previewVisible: false,
|
previewRow: {},
|
previewUrl: ''
|
}
|
},
|
computed: {
|
currentId() {
|
const field = this.idField || this.idParam
|
return this.currentRow ? this.currentRow[field] : undefined
|
},
|
canUpload() {
|
return !this.readonly && (!!this.currentId || this.deferUpload)
|
},
|
displayList() {
|
const pending = this.pendingFiles.map(item => ({
|
id: 'pending_' + item.uid,
|
uid: item.uid,
|
fileName: item.fileName,
|
createTime: '待上传',
|
pending: true
|
}))
|
return pending.concat(this.attachmentList)
|
},
|
dialogTitle() {
|
const name = this.nameField && this.currentRow ? this.currentRow[this.nameField] : ''
|
return this.title + (name ? ' - ' + name : '')
|
},
|
uploadAction() {
|
return this.javaApi + this.uploadUrl + '?' + this.idParam + '=' + this.currentId
|
}
|
},
|
methods: {
|
// 打开附件管理弹窗
|
open(row) {
|
this.currentRow = row
|
this.visible = true
|
this.loadList()
|
},
|
// 预览附件入口:只有一个附件直接预览,多个打开弹窗
|
previewAttachments(row) {
|
this.queryList(row).then(list => {
|
if (!list.length) {
|
this.$message.warning('暂无附件')
|
} else if (list.length === 1) {
|
this.previewFile(list[0])
|
} else {
|
this.open(row)
|
}
|
})
|
},
|
// 下载附件入口:只有一个附件直接下载,多个打开弹窗
|
downloadAttachments(row) {
|
this.queryList(row).then(list => {
|
if (!list.length) {
|
this.$message.warning('暂无附件')
|
} else if (list.length === 1) {
|
this.downloadFile(list[0])
|
} else {
|
this.open(row)
|
}
|
})
|
},
|
queryList(row) {
|
const field = this.idField || this.idParam
|
return request({
|
url: this.selectUrl,
|
method: 'get',
|
params: { [this.idParam]: row[field] }
|
}).then(res => (res && res.data) || [])
|
},
|
loadList() {
|
this.queryList(this.currentRow).then(list => {
|
this.attachmentList = list
|
})
|
},
|
handleUploadSuccess(res) {
|
if (res.code === 200) {
|
this.$message.success('上传成功')
|
this.loadList()
|
} else {
|
this.$message.error(res.msg || '上传失败')
|
}
|
},
|
handleUploadError() {
|
this.$message.error('上传失败,文件可能超过200MB')
|
},
|
// 延迟上传模式:文件先暂存在页面,提交保存时再统一上传
|
handleFileChange(file) {
|
if (!this.deferUpload || !file || !file.raw) {
|
return
|
}
|
this.pendingFiles.push({ uid: file.uid, fileName: file.name, raw: file.raw })
|
},
|
removePending(row) {
|
this.pendingFiles = this.pendingFiles.filter(item => item.uid !== row.uid)
|
},
|
// 提交时调用:把暂存的附件上传到指定主表ID,全部成功才算完成
|
flushPending(planDetailsId) {
|
if (!this.pendingFiles.length) {
|
return Promise.resolve()
|
}
|
const field = this.idField || this.idParam
|
this.currentRow = { [field]: planDetailsId }
|
return Promise.all(this.pendingFiles.slice().map(item => {
|
const formData = new FormData()
|
formData.append('file', item.raw)
|
return request({
|
url: this.uploadUrl,
|
method: 'post',
|
params: { [this.idParam]: planDetailsId },
|
data: formData,
|
headers: { 'Content-Type': 'multipart/form-data', repeatSubmit: false }
|
}).then(res => {
|
if (!res || res.code !== 200) {
|
throw new Error((res && res.msg) || ('附件“' + item.fileName + '”上传失败'))
|
}
|
this.pendingFiles = this.pendingFiles.filter(pending => pending.uid !== item.uid)
|
})
|
})).then(() => {
|
this.loadList()
|
})
|
},
|
previewFile(row) {
|
this.previewRow = row
|
this.previewUrl = this.fullUrl(row.fileUrl)
|
this.previewVisible = true
|
},
|
downloadFile(row) {
|
this.$download.saveAs(this.fullUrl(row.fileUrl), row.fileName)
|
},
|
removeFile(row) {
|
this.$confirm('是否删除该附件?', '提示', { type: 'warning' }).then(() => {
|
request({
|
url: this.deleteUrl,
|
method: 'delete',
|
params: { id: row.id }
|
}).then(() => {
|
this.$message.success('删除成功')
|
this.loadList()
|
})
|
}).catch(() => {})
|
},
|
// 静态地址拼接,与后端上传统一路由保持一致:图片在 /img,其它都在 /word
|
fullUrl(fileUrl) {
|
const base = /\.(jpg|jpeg|png|gif)$/i.test(fileUrl) ? '/img/' : '/word/'
|
return this.javaApi + base + fileUrl
|
}
|
}
|
}
|
</script>
|
|
<style scoped>
|
.lims-attachment {
|
width: 100%;
|
}
|
|
.attachment-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
}
|
|
.attachment-inline {
|
width: 100%;
|
}
|
</style>
|