<script lang="ts" setup>
|
import type { QcReportInstanceApi } from '#/api/mes/qc/report/instance';
|
|
import { computed, ref } from 'vue';
|
|
import { useVbenModal } from '@vben/common-ui';
|
import { IconifyIcon } from '@vben/icons';
|
import { formatFileSize, openSafeUrl } from '@vben/utils';
|
|
import { DICT_TYPE } from '@vben/constants';
|
|
import { Alert, Button, Descriptions, message, Spin, Tabs, Tag } from 'ant-design-vue';
|
|
import {
|
exportInstancePdf,
|
getInstance,
|
getInstanceSnapshot,
|
} from '#/api/mes/qc/report/instance';
|
import { DictTag } from '#/components/dict-tag';
|
|
import { businessTypeLabel } from '../data';
|
|
defineOptions({ name: 'QcReportInstanceDetail' });
|
|
const detail = ref<QcReportInstanceApi.InstanceDetail>();
|
const snapshot = ref<Record<string, unknown>>();
|
const activeTab = ref('preview');
|
const loading = ref(false);
|
const exporting = ref(false);
|
const pdf = ref<QcReportInstanceApi.PdfResult>();
|
|
/** 产物 HTML 是出件时冻结下来的那一份,直接渲染即为预览,不必重新出件 */
|
const renderHtml = computed(() => detail.value?.renderHtml ?? '');
|
|
const snapshotText = computed(() =>
|
snapshot.value ? JSON.stringify(snapshot.value, null, 2) : '',
|
);
|
|
const [Modal, modalApi] = useVbenModal({
|
async onOpenChange(isOpen: boolean) {
|
if (!isOpen) {
|
detail.value = undefined;
|
snapshot.value = undefined;
|
pdf.value = undefined;
|
activeTab.value = 'preview';
|
return;
|
}
|
const data = modalApi.getData<QcReportInstanceApi.Instance>();
|
if (!data?.id) {
|
return;
|
}
|
modalApi.lock();
|
loading.value = true;
|
try {
|
// 两个接口分开取:详情里有几 MB 的产物 HTML,快照也不小,合成一个响应会让这份数据每次都被整包传输
|
const [loadedDetail, loadedSnapshot] = await Promise.all([
|
getInstance(data.id),
|
getInstanceSnapshot(data.id),
|
]);
|
detail.value = loadedDetail;
|
snapshot.value = loadedSnapshot;
|
} finally {
|
loading.value = false;
|
modalApi.unlock();
|
}
|
},
|
});
|
|
/** 导出 PDF:后端会先删旧的归档附件再绑定新的,所以重复点不会在磁盘上堆文件 */
|
async function handleExportPdf() {
|
if (!detail.value?.id) {
|
return;
|
}
|
exporting.value = true;
|
try {
|
pdf.value = await exportInstancePdf(detail.value.id);
|
message.success(
|
`已生成 ${pdf.value.fileName}(${formatFileSize(pdf.value.byteSize)},耗时 ${pdf.value.durationMs} 毫秒)`,
|
);
|
} finally {
|
exporting.value = false;
|
}
|
}
|
|
function handleOpenPreview() {
|
openSafeUrl(pdf.value?.previewURL);
|
}
|
|
function handleDownload() {
|
openSafeUrl(pdf.value?.downloadURL);
|
}
|
</script>
|
|
<template>
|
<Modal :title="`报告详情${detail?.reportNo ? ` - ${detail.reportNo}` : ''}`" class="w-4/5">
|
<Spin :spinning="loading">
|
<div class="flex flex-col gap-3">
|
<Descriptions bordered size="small" :column="3">
|
<Descriptions.Item label="报告编号">
|
{{ detail?.reportNo || '—' }}
|
</Descriptions.Item>
|
<Descriptions.Item label="模板版本">
|
<Tag v-if="detail?.templateVersion" color="green">
|
{{ detail.templateVersion }}
|
</Tag>
|
<span v-else>—</span>
|
</Descriptions.Item>
|
<Descriptions.Item label="报告状态">
|
<DictTag
|
v-if="detail?.status !== undefined && detail?.status !== null"
|
:type="DICT_TYPE.QC_REPORT_INSTANCE_STATUS"
|
:value="detail.status"
|
/>
|
<span v-else>—</span>
|
</Descriptions.Item>
|
<Descriptions.Item label="业务类型">
|
{{ businessTypeLabel(detail?.businessType) || '—' }}
|
</Descriptions.Item>
|
<Descriptions.Item label="业务单据">
|
{{ detail?.businessId || '—' }}
|
</Descriptions.Item>
|
<Descriptions.Item label="生成时间">
|
{{ detail?.createTime || '—' }}
|
</Descriptions.Item>
|
</Descriptions>
|
|
<Tabs v-model:active-key="activeTab" type="card">
|
<Tabs.TabPane key="preview" tab="报告预览" force-render>
|
<Alert
|
v-if="!renderHtml"
|
type="warning"
|
show-icon
|
message="这份报告没有可预览的产物"
|
description="实例的渲染产物为空,通常意味着出件当时未成功。请回到列表确认报告状态,或重新出件。"
|
/>
|
<!--
|
sandbox 留空 = 彻底禁用脚本、表单与同源,只让 CSS 生效:
|
预览的是渲染产物,不需要也不应该让它执行任何东西。
|
-->
|
<iframe
|
v-else
|
:srcdoc="renderHtml"
|
sandbox=""
|
class="h-[60vh] w-full rounded border border-border bg-white"
|
></iframe>
|
</Tabs.TabPane>
|
|
<Tabs.TabPane key="snapshot" tab="数据快照">
|
<Alert
|
class="mb-2"
|
type="info"
|
show-icon
|
message="这是出件时冻结的业务数据,不会随后续单据修改而变化"
|
description="查看「这份历史报告当时用的是哪些数据」请以这里为准。"
|
/>
|
<pre
|
class="max-h-[55vh] overflow-auto rounded border border-border bg-gray-50 p-3 text-xs"
|
>{{ snapshotText || '(无快照数据)' }}</pre
|
>
|
</Tabs.TabPane>
|
</Tabs>
|
|
<Alert v-if="pdf" type="success" show-icon>
|
<template #message>
|
<div class="text-xs">
|
<div>
|
已生成 {{ pdf.fileName }}({{ formatFileSize(pdf.byteSize) }},打印耗时
|
{{ pdf.pdfDurationMs }} 毫秒)
|
</div>
|
<div class="mt-1 flex gap-2">
|
<Button size="small" @click="handleOpenPreview">在线预览</Button>
|
<Button size="small" type="primary" @click="handleDownload">
|
下载 PDF
|
</Button>
|
</div>
|
</div>
|
</template>
|
</Alert>
|
</div>
|
</Spin>
|
|
<template #footer>
|
<div class="flex justify-end gap-2">
|
<Button @click="modalApi.close()">关闭</Button>
|
<Button
|
type="primary"
|
:loading="exporting"
|
:disabled="!detail?.id"
|
@click="handleExportPdf"
|
>
|
<IconifyIcon icon="lucide:file-down" class="mr-1" />
|
导出 PDF
|
</Button>
|
</div>
|
</template>
|
</Modal>
|
</template>
|