src/components/Attachment/lims-attachment.vue
@@ -1,10 +1,40 @@
<template>
  <div>
    <el-dialog :title="dialogTitle" v-model="visible" width="760px">
  <div class="lims-attachment">
    <div v-if="inline" class="attachment-inline">
      <div class="attachment-header">
        <div class="el-upload__tip">{{ tip }}</div>
        <el-upload :action="uploadAction" :headers="uploadHeader" multiple :show-file-list="false"
          :on-success="handleUploadSuccess" :on-error="handleUploadError">
        <div class="el-upload__tip">
          {{ readonly ? '仅可预览、下载附件' : ((currentId || deferUpload) ? tip : emptyTip) }}
        </div>
        <el-upload v-if="!readonly" :action="uploadAction" :headers="uploadHeader" multiple :accept="accept"
          :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="primary" @click="previewPending(scope.row)">预览</el-button>
              <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 :accept="accept"
          :show-file-list="false" :on-success="handleUploadSuccess" :on-error="handleUploadError">
          <el-button size="small" type="primary">上传附件</el-button>
        </el-upload>
      </div>
@@ -15,7 +45,7 @@
          <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 link type="danger" @click="removeFile(scope.row)">删除</el-button>
            <el-button v-if="!readonly" link type="danger" @click="removeFile(scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
@@ -23,7 +53,7 @@
        <el-button @click="visible = false">关 闭</el-button>
      </template>
    </el-dialog>
    <el-dialog title="预览附件" v-model="previewVisible" fullscreen append-to-body>
    <el-dialog title="预览附件" v-model="previewVisible" fullscreen append-to-body @close="releasePreviewBlob">
      <file-preview v-if="previewVisible" :file-url="previewUrl" :current-file="previewRow" />
    </el-dialog>
  </div>
@@ -31,6 +61,7 @@
<script>
import request from '@/utils/request'
import { resolveAttachmentUrl } from '@/utils/attachment'
import filePreview from '@/components/Preview/filePreview.vue'
/**
@@ -51,6 +82,7 @@
  components: {
    filePreview
  },
  emits: ['closed'],
  props: {
    // 上传接口地址,如 /supplierManagement/uploadAttachment
    uploadUrl: {
@@ -91,22 +123,83 @@
    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
    },
    // 允许选择的文件类型,如 .jpg,.png;为空不限制
    accept: {
      type: String,
      default: ''
    }
  },
  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: ''
      previewUrl: '',
      previewBlobUrl: ''
    }
  },
  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] : ''
@@ -115,6 +208,9 @@
    uploadAction() {
      return this.javaApi + this.uploadUrl + '?' + this.idParam + '=' + this.currentId
    }
  },
  beforeUnmount() {
    this.releasePreviewBlob()
  },
  methods: {
    // 打开附件管理弹窗
@@ -171,13 +267,68 @@
    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()
      })
    },
    // 待上传文件用本地临时地址预览,提交后再走正式地址
    previewPending(row) {
      const item = this.pendingFiles.find(pending => pending.uid === row.uid)
      if (!item) {
        return
      }
      this.releasePreviewBlob()
      this.previewBlobUrl = URL.createObjectURL(item.raw)
      this.previewRow = { fileName: item.fileName, fileUrl: this.previewBlobUrl }
      this.previewUrl = this.previewBlobUrl
      this.previewVisible = true
    },
    releasePreviewBlob() {
      if (this.previewBlobUrl) {
        URL.revokeObjectURL(this.previewBlobUrl)
        this.previewBlobUrl = ''
      }
    },
    previewFile(row) {
      this.releasePreviewBlob()
      this.previewRow = row
      this.previewUrl = this.fullUrl(row.fileUrl)
      this.previewVisible = true
    },
    downloadFile(row) {
      this.$download.saveAs(this.fullUrl(row.fileUrl), row.fileName)
      return this.$download.attachment(row, row.fileName)
    },
    removeFile(row) {
      this.$confirm('是否删除该附件?', '提示', { type: 'warning' }).then(() => {
@@ -193,17 +344,24 @@
    },
    // 静态地址拼接,与后端上传统一路由保持一致:图片在 /img,其它都在 /word
    fullUrl(fileUrl) {
      const base = /\.(jpg|jpeg|png|gif)$/i.test(fileUrl) ? '/img/' : '/word/'
      return this.javaApi + base + fileUrl
      return resolveAttachmentUrl(fileUrl)
    }
  }
}
</script>
<style scoped>
.lims-attachment {
  width: 100%;
}
.attachment-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.attachment-inline {
  width: 100%;
}
</style>