212 lines
5.9 KiB
Vue
212 lines
5.9 KiB
Vue
<template>
|
||
<a-modal
|
||
v-model:open="innerOpen"
|
||
:title="modalTitle"
|
||
width="960px"
|
||
:footer="null"
|
||
destroy-on-close
|
||
wrap-class-name="raw-material-card-print-preview-modal"
|
||
@cancel="onClose"
|
||
>
|
||
<a-spin :spinning="loading">
|
||
<div v-if="errorText" class="preview-error">{{ errorText }}</div>
|
||
<div v-else class="preview-body">
|
||
<iframe
|
||
v-if="previewHtml"
|
||
ref="previewIframeRef"
|
||
class="preview-iframe"
|
||
title="原材料卡片打印预览"
|
||
:srcdoc="previewHtml"
|
||
/>
|
||
<a-empty v-else-if="!loading" description="暂无预览内容" />
|
||
</div>
|
||
</a-spin>
|
||
<div class="preview-footer">
|
||
<a-space>
|
||
<a-button @click="innerOpen = false">关闭</a-button>
|
||
<a-button type="primary" :disabled="!previewHtml || !!errorText" @click="handleBrowserPrint">
|
||
<Icon icon="ant-design:printer-outlined" />
|
||
浏览器打印
|
||
</a-button>
|
||
</a-space>
|
||
</div>
|
||
</a-modal>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { computed, ref, watch } from 'vue';
|
||
import { Icon } from '/@/components/Icon';
|
||
import { useMessage } from '/@/hooks/web/useMessage';
|
||
import { prepareNativePrint } from '../MesXslRawMaterialCard.api';
|
||
import { renderNativePrintHtml } from '/@/views/print/template/native/core/printRenderer';
|
||
import { normalizeImportedNativeSchema } from '/@/views/print/template/native/core/nativeSchemaNormalize';
|
||
|
||
const props = defineProps<{
|
||
open: boolean;
|
||
/** 卡片主键,有值时拉取模板与业务数据并渲染 */
|
||
cardId: string | null;
|
||
/** 展示在标题上的条码/说明 */
|
||
barcode?: string;
|
||
}>();
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'update:open', v: boolean): void;
|
||
}>();
|
||
|
||
const { createMessage } = useMessage();
|
||
|
||
const innerOpen = computed({
|
||
get: () => props.open,
|
||
set: (v: boolean) => emit('update:open', v),
|
||
});
|
||
|
||
const modalTitle = computed(() => {
|
||
const b = String(props.barcode || '').trim();
|
||
return b ? `原材料卡片打印预览(条码:${b})` : '原材料卡片打印预览';
|
||
});
|
||
|
||
const loading = ref(false);
|
||
const errorText = ref('');
|
||
const previewHtml = ref('');
|
||
const previewIframeRef = ref<HTMLIFrameElement | null>(null);
|
||
|
||
async function loadPreview(id: string) {
|
||
loading.value = true;
|
||
errorText.value = '';
|
||
previewHtml.value = '';
|
||
try {
|
||
const prep = (await prepareNativePrint(id)) as Record<string, unknown>;
|
||
const templateJsonRaw = prep.templateJson as string;
|
||
const printData = prep.printData as Record<string, unknown>;
|
||
if (!templateJsonRaw) {
|
||
throw new Error('模板 JSON 为空,请检查「业务打印绑定」是否已配置');
|
||
}
|
||
let raw: unknown;
|
||
try {
|
||
raw = typeof templateJsonRaw === 'string' ? JSON.parse(templateJsonRaw) : templateJsonRaw;
|
||
} catch {
|
||
throw new Error('模板 JSON 格式错误');
|
||
}
|
||
const schema = normalizeImportedNativeSchema(raw);
|
||
previewHtml.value = await renderNativePrintHtml(schema, printData as Record<string, unknown>);
|
||
} catch (e: unknown) {
|
||
errorText.value = e instanceof Error ? e.message : String(e);
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 在 Modal 内对 iframe 直接 print() 时,打印对话框易被遮罩层级挡住或焦点异常,表现为「点了没反应」。
|
||
* 改为在 body 下挂临时 iframe、写入同一套 HTML 再打印,与模板预览常用做法一致。
|
||
*/
|
||
function handleBrowserPrint() {
|
||
const html = previewHtml.value;
|
||
if (!html?.trim()) {
|
||
createMessage.warning('预览未就绪,请稍后再试');
|
||
return;
|
||
}
|
||
const iframe = document.createElement('iframe');
|
||
iframe.setAttribute(
|
||
'style',
|
||
'position:fixed;left:0;top:0;width:0;height:0;border:0;opacity:0;pointer-events:none;',
|
||
);
|
||
document.body.appendChild(iframe);
|
||
const doc = iframe.contentDocument;
|
||
if (!doc) {
|
||
document.body.removeChild(iframe);
|
||
createMessage.error('无法创建打印文档');
|
||
return;
|
||
}
|
||
try {
|
||
doc.open();
|
||
doc.write(html);
|
||
doc.close();
|
||
} catch {
|
||
document.body.removeChild(iframe);
|
||
createMessage.error('写入打印内容失败');
|
||
return;
|
||
}
|
||
|
||
const cleanup = () => {
|
||
try {
|
||
if (iframe.parentNode) {
|
||
document.body.removeChild(iframe);
|
||
}
|
||
} catch {
|
||
// ignore
|
||
}
|
||
};
|
||
|
||
const runPrint = () => {
|
||
try {
|
||
const w = iframe.contentWindow;
|
||
if (!w) {
|
||
createMessage.error('无法唤起打印窗口');
|
||
cleanup();
|
||
return;
|
||
}
|
||
w.focus();
|
||
w.print();
|
||
/** 关闭打印对话框后移除临时 iframe(部分浏览器支持 afterprint) */
|
||
w.addEventListener('afterprint', cleanup, { once: true });
|
||
window.setTimeout(cleanup, 120000);
|
||
} catch {
|
||
createMessage.error('无法唤起打印,请检查浏览器弹窗/打印权限');
|
||
cleanup();
|
||
}
|
||
};
|
||
|
||
/** 等待排版与字体后再打印,减少空白页 */
|
||
window.setTimeout(runPrint, 100);
|
||
}
|
||
|
||
function onClose() {
|
||
errorText.value = '';
|
||
previewHtml.value = '';
|
||
}
|
||
|
||
watch(
|
||
() => [props.open, props.cardId] as const,
|
||
([isOpen, id]) => {
|
||
if (isOpen && id) {
|
||
void loadPreview(id);
|
||
}
|
||
if (!isOpen) {
|
||
onClose();
|
||
}
|
||
},
|
||
);
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.preview-error {
|
||
color: #cf1322;
|
||
padding: 8px 0;
|
||
}
|
||
|
||
.preview-body {
|
||
min-height: 420px;
|
||
max-height: 72vh;
|
||
overflow: auto;
|
||
border: 1px solid #f0f0f0;
|
||
border-radius: 4px;
|
||
background: #fafafa;
|
||
}
|
||
|
||
.preview-iframe {
|
||
display: block;
|
||
width: 100%;
|
||
min-height: 400px;
|
||
border: 0;
|
||
background: #fff;
|
||
}
|
||
|
||
.preview-footer {
|
||
margin-top: 16px;
|
||
padding-top: 12px;
|
||
border-top: 1px solid #f0f0f0;
|
||
text-align: right;
|
||
}
|
||
</style>
|