Эх сурвалжийг харах

feat(inspectionProjectRequest): 支持受托方回传检验报告的预览与下载

江南 QMS 回传的是整份报告文件(base64 存在请托单 report_files),与系统内模板报告快照是两套数据,页面并存展示。

- 新增 reportFileDialog:列出报告文件(名称/大小/接收时间),pdf 与图片可预览,其余仅下载;api 增加 blob 取数、预览地址、下载三个方法,正文按 hash 单取。

- 请托单列表新增「检验报告」入口;模板报告与回传文件入口按 hasTemplateReportData 分流:模板报告优先,模板入口不可见时才露出文件入口。

- 列表分页联表查询取不到 reportTemplateJson(JSON 列恒为空),原 isEmptyObject 判断失效,改用后端 hasTemplateReport 标记;openReport 时用详情数据回填 reportTemplateJson/reportTemplateCode。

- 报工弹窗 toolbar 同步增加「检验报告」入口,复用同一弹窗。
wuzh 5 өдөр өмнө
parent
commit
4337033dea

+ 34 - 0
src/api/inspectionProjectRequest/index.js

@@ -1,4 +1,5 @@
 import request from '@/utils/request';
+import { download } from '@/utils/file';
 
 // 列表
 
@@ -62,3 +63,36 @@ export async function removeItem(data) {
   }
   return Promise.reject(new Error(res.data.message));
 }
+
+/**
+ * 取外部(江南)回传的检验报告文件内容。
+ *
+ * 本系统自己的检测报告是模板快照(reportTemplateJson),江南回传的是对方产出的整份文件,
+ * 以 Base64 存在请托单的 report_files 里,因此正文按 hash 单取,列表只带文件名/摘要/大小。
+ *
+ * @param {Number|String} id 请托单ID
+ * @param {String} hash 报告摘要,不传取第一份
+ * @returns {Promise<Blob>}
+ */
+export async function getReportFileBlob(id, hash) {
+  const res = await request.get(`/qms/requestentrust/reportFile/${id}`, {
+    params: { hash },
+    responseType: 'blob'
+  });
+  return res.data;
+}
+
+/** 下载检验报告到本地。 */
+export async function downloadReportFile(id, hash, fileName) {
+  const blob = await getReportFileBlob(id, hash);
+  download(blob, fileName || '理化检验报告');
+}
+
+/**
+ * 取检验报告的可预览地址(blob URL,调用方用完要 revoke)。
+ * pdf / 图片浏览器可直接内嵌预览,其他格式请改用下载。
+ */
+export async function getReportFilePreviewUrl(id, hash) {
+  const blob = await getReportFileBlob(id, hash);
+  return window.URL.createObjectURL(blob);
+}

+ 79 - 11
src/views/inspectionProjectRequest/components/inspectionProjectRequestList.vue

@@ -122,7 +122,7 @@
           v-if="
             (!row.reportApprovalStatus || row.reportApprovalStatus == 0) &&
             row.status == 2 &&
-            isEmptyObject(row.reportTemplateJson) &&
+            !hasTemplateReportData(row) &&
             [2].includes(row.approvalStatus) &&
             pageName == 'inspectionProjectEntrusted' &&
             $hasPermission('qms:quality_work_order:generateReport')
@@ -147,24 +147,26 @@
           </el-dropdown-menu>
         </el-dropdown>
         <el-link
-          v-if="
-            row.reportApprovalStatus &&
-            row.reportApprovalStatus != 3 &&
-            !isEmptyObject(row.reportTemplateJson) &&
-            row.status == 2 &&
-            [2].includes(row.approvalStatus) &&
-            pageName == 'inspectionProjectEntrusted' &&
-            $hasPermission('qms:quality_work_order:generateReport')
-          "
+          v-if="canViewTemplateReport(row)"
           type="primary"
           :underline="false"
           @click="openReport(row)"
           >查看检测报告</el-link
         >
+        <!-- 外部(江南)回传的检验报告:整份文件。单据本身有模板报告时以模板为准,不再重复露出 -->
+        <el-link
+          v-if="canViewReportFile(row)"
+          type="primary"
+          :underline="false"
+          @click="openReportFiles(row)"
+          >检验报告</el-link
+        >
 
+        <!-- 检测报告审批只针对我方模板报告;没有模板数据(走江南回传文件)的单据不出现该入口 -->
         <el-link
           v-if="
             (!row.reportApprovalStatus || row.reportApprovalStatus == 3) &&
+            hasTemplateReportData(row) &&
             row.status == 2 &&
             [2].includes(row.approvalStatus) &&
             pageName == 'inspectionProjectEntrusted' &&
@@ -213,6 +215,7 @@
       @reload="reload"
     ></ReportTemplateWrapper>
     <sampleReport ref="sampleReportRef" @reload="reload"></sampleReport>
+    <reportFileDialog ref="reportFileDialogRef"></reportFileDialog>
 
     <Transfer
       v-if="transferVisible"
@@ -245,6 +248,7 @@
   import Transfer from './transfer.vue';
   import sampleReport from '@/views/inspectionWork/components/sampleReport.vue';
   import ReportTemplateWrapper from '@/views/inspectionReport/components/reportTemplateWrapper.vue';
+  import reportFileDialog from './reportFileDialog.vue';
   import { queryTodo } from '@/api/bpm/task';
 
   import tabMixins from '@/mixins/tableColumnsMixin';
@@ -276,7 +280,8 @@
       Transfer,
       bpmDetail,
       sampleReport,
-      ReportTemplateWrapper
+      ReportTemplateWrapper,
+      reportFileDialog
     },
     mixins: [dictMixins, tabMixins],
     props: {
@@ -703,8 +708,71 @@
       async openReport(row) {
         const data = await getById(row.id);
         row.reportApprovalTaskVos = data.reportApprovalTaskVos || {};
+        // 列表接口只回「有没有模板报告」的标记(JSON 列在联表查询里拿不到),正文用详情数据覆盖
+        row.reportTemplateJson = data.reportTemplateJson || row.reportTemplateJson;
+        row.reportTemplateCode = data.reportTemplateCode || row.reportTemplateCode;
         this.$refs.targetComponentRef.open({ row, item: {} }, true);
       },
+      /* 外部回传的检验报告(江南理化委托),整份文件 */
+      openReportFiles(row) {
+        this.$refs.reportFileDialogRef.open({
+          id: row.id,
+          code: row.code,
+          files: row.reportFiles
+        });
+      },
+      /**
+       * 单据是否已有我方模板检测报告数据。
+       *
+       * 江南回传报文里没有模板信息,模板报告来自正常走系统内部请托生成的那一份,
+       * 因此一张单可能「既有模板报告、又有江南文件」,展示时以模板报告为准。
+       * 优先用后端回填的 hasTemplateReport 标记:分页联表查询拿不到 JSON 列,
+       * 列表里的 reportTemplateJson 恒为空,不能用来判断。
+       */
+      hasTemplateReportData(row) {
+        if (!row) {
+          return false;
+        }
+        if (typeof row.hasTemplateReport === 'boolean') {
+          return row.hasTemplateReport;
+        }
+        const json = row.reportTemplateJson;
+        if (!json) {
+          return false;
+        }
+        if (Array.isArray(json)) {
+          return json.length > 0;
+        }
+        if (typeof json !== 'object') {
+          return String(json).trim().length > 0 && String(json).trim() !== '{}';
+        }
+        return Object.keys(json).length > 0;
+      },
+      /* 模板检测报告入口:有模板数据 + 已报工 + 请托审批通过 + 受托页 + 报告查看权限 */
+      canViewTemplateReport(row) {
+        return (
+          !!row &&
+          !!row.reportApprovalStatus &&
+          row.reportApprovalStatus != 3 &&
+          this.hasTemplateReportData(row) &&
+          row.status == 2 &&
+          [2].includes(row.approvalStatus) &&
+          this.pageName == 'inspectionProjectEntrusted' &&
+          this.$hasPermission('qms:quality_work_order:generateReport')
+        );
+      },
+      /**
+       * 江南文件报告入口:有回传文件,且当前单据看不到模板报告。
+       *
+       * 模板入口不可见的两种情形都要露出文件入口,避免两边都看不到:
+       * 单据本身没有模板报告数据;或者当前用户/当前页面没有模板报告入口(如检测请托页)。
+       */
+      canViewReportFile(row) {
+        if (!row || !row.reportFiles || !row.reportFiles.length) {
+          return false;
+        }
+        return !this.canViewTemplateReport(row);
+      },
 
       isEmptyObject(obj = {}) {
         return Object.keys(obj).length === 0;

+ 131 - 0
src/views/inspectionProjectRequest/components/reportFileDialog.vue

@@ -0,0 +1,131 @@
+<template>
+  <ele-modal
+    :visible.sync="visible"
+    width="620px"
+    append-to-body
+    title="检验报告"
+    :close-on-click-modal="false"
+  >
+    <el-table :data="files" size="small" border empty-text="暂无检验报告">
+      <el-table-column type="index" label="序号" width="60" align="center" />
+      <el-table-column
+        prop="name"
+        label="报告文件"
+        show-overflow-tooltip
+        min-width="220"
+      />
+      <el-table-column label="大小" width="100" align="center">
+        <template v-slot="{ row }">{{ formatSize(row) }}</template>
+      </el-table-column>
+      <el-table-column
+        prop="saveTime"
+        label="接收时间"
+        width="160"
+        align="center"
+      />
+      <el-table-column label="操作" width="140" align="center">
+        <template v-slot="{ row }">
+          <el-link
+            v-if="canPreview(row)"
+            type="primary"
+            :underline="false"
+            @click="preview(row)"
+            >预览</el-link
+          >
+          <el-link type="primary" :underline="false" @click="downloadFile(row)"
+            >下载</el-link
+          >
+        </template>
+      </el-table-column>
+    </el-table>
+    <div class="tip">
+      检验报告由受托方(江南 QMS)回传,系统只做接收与留档,不支持编辑。单据若已生成我方模板检测报告,以模板报告为准,此处为对方回传的原始文件。
+    </div>
+
+    <template v-slot:footer>
+      <el-button @click="visible = false">关闭</el-button>
+    </template>
+  </ele-modal>
+</template>
+
+<script>
+  import {
+    downloadReportFile,
+    getReportFilePreviewUrl
+  } from '@/api/inspectionProjectRequest';
+
+  /**
+   * 外部(江南)回传的检验报告文件列表。
+   *
+   * 本系统自己的检测报告是模板快照,走 reportTemplateWrapper 渲染;
+   * 江南回传的是整份文件,这里只负责列出、预览、下载,两者在页面上并存。
+   */
+  export default {
+    name: 'ReportFileDialog',
+    data() {
+      return {
+        visible: false,
+        entrustId: null,
+        code: '',
+        files: []
+      };
+    },
+    methods: {
+      open({ id, code, files } = {}) {
+        this.entrustId = id;
+        this.code = code || '';
+        this.files = Array.isArray(files) ? files : [];
+        this.visible = true;
+      },
+      /** base64Length 换算成字节约为 3/4,取不到时显示 - */
+      formatSize(row) {
+        const len = Number(row.base64Length);
+        if (!len) {
+          return '-';
+        }
+        const bytes = Math.floor((len * 3) / 4);
+        if (bytes < 1024) {
+          return bytes + ' B';
+        }
+        if (bytes < 1024 * 1024) {
+          return (bytes / 1024).toFixed(1) + ' KB';
+        }
+        return (bytes / 1024 / 1024).toFixed(2) + ' MB';
+      },
+      /** 浏览器能直接内嵌预览的格式:pdf 与常见图片 */
+      canPreview(row) {
+        const name = (row.name || '').toLowerCase();
+        return /\.(pdf|png|jpe?g|gif|bmp|webp)$/.test(name);
+      },
+      async preview(row) {
+        const loading = this.$loading({ lock: true });
+        try {
+          const url = await getReportFilePreviewUrl(this.entrustId, row.hash);
+          window.open(url, '_blank');
+        } catch (e) {
+          this.$message.error('预览失败,请下载后查看');
+        } finally {
+          loading.close();
+        }
+      },
+      async downloadFile(row) {
+        const loading = this.$loading({ lock: true });
+        try {
+          await downloadReportFile(this.entrustId, row.hash, row.name);
+        } catch (e) {
+          this.$message.error('下载失败:' + (e.message || '报告不存在'));
+        } finally {
+          loading.close();
+        }
+      }
+    }
+  };
+</script>
+
+<style scoped lang="scss">
+  .tip {
+    margin-top: 10px;
+    color: #909399;
+    font-size: 12px;
+  }
+</style>

+ 20 - 1
src/views/inspectionWork/components/inspectionProjectReport.vue

@@ -18,6 +18,15 @@
     >
       <template v-slot:toolbar>
         样品总数:{{ form.workSampleCount }}{{ form.measureUnit }}
+        <!-- 受托方(江南 QMS)回传的检验报告,整份文件 -->
+        <el-link
+          v-if="form.reportFiles && form.reportFiles.length"
+          type="primary"
+          :underline="false"
+          style="margin-left: 16px"
+          @click="openReportFiles"
+          >检验报告</el-link
+        >
       </template>
       <template v-slot:craftFiles="{ row }">
         <el-link type="primary" :underline="false" @click="craftFiles(row)"
@@ -281,6 +290,7 @@
       :pageName="pageName"
     ></experimentReport>
     <wokePopup ref="wokePopupRef"></wokePopup>
+    <reportFileDialog ref="reportFileDialogRef"></reportFileDialog>
   </ele-modal>
 </template>
 
@@ -289,6 +299,7 @@
   import detailMixins from '../mixins/detailMixins';
   import toolButtom from './toolButtom.vue';
   import experimentReport from './experimentReport.vue';
+  import reportFileDialog from '@/views/inspectionProjectRequest/components/reportFileDialog.vue';
   import { handleInput } from './unit.js';
   import {
     exeTaskReportWork,
@@ -301,7 +312,7 @@
   export default {
     mixins: [dictMixins, detailMixins],
 
-    components: { toolButtom, experimentReport, wokePopup },
+    components: { toolButtom, experimentReport, wokePopup, reportFileDialog },
 
     data() {
       return {
@@ -777,6 +788,14 @@
       });
     },
     methods: {
+      /* 受托方回传的检验报告(整份文件),与模板检测报告并存 */
+      openReportFiles() {
+        this.$refs.reportFileDialogRef.open({
+          id: this.form.id,
+          code: this.form.code,
+          files: this.form.reportFiles
+        });
+      },
       async craftFiles(row) {
         const res = await parameterGetByCode({
           code: 'mes_craft_file_by_category_code'