<script lang="ts" setup>
|
import type { LabelLayoutConfig } from './labelLayout';
|
|
import { computed, onUnmounted, reactive, toRefs, unref, watch } from 'vue';
|
import { hiprint } from 'vue-plugin-hiprint';
|
|
import { useGlobSetting, useMessage } from '@jnpf/hooks';
|
import { BasicModal, useModalInner } from '@jnpf/ui/modal';
|
|
import { useUserStore } from '@vben/stores';
|
|
import dayjs from 'dayjs';
|
import $ from 'jquery';
|
|
import { getBatchData } from '#/api/system/printDev';
|
import SystemBrandImage from '#/components/SystemBrandImage/index.vue';
|
import { useFlowState } from '#/hooks/flow/useFlowStatus';
|
import { $t } from '#/locales';
|
import { getAuthMediaUrl, onlineUtils } from '#/utils/jnpf';
|
|
import { buildLabelLayoutHtml, normalizeLabelLayoutConfig } from './labelLayout';
|
import { exportLabelLayoutPdf } from './labelPdf';
|
import { applyMergedBatchPrintLayout, getMergedBatchPrintTableStyle, mergePrintDataSet } from './printData';
|
|
interface State {
|
fullName: any;
|
hiprintTemplate: any;
|
hasActiveSliceConfig: boolean;
|
dataList: any[];
|
loading: boolean;
|
labelLayout: LabelLayoutConfig;
|
labelLayoutHtml: string;
|
layoutError: string;
|
mergeDataSet: string;
|
onPrint?: (printData: any[], metadata: { fullName: string; templateId: string }) => void;
|
templateId: string;
|
systemInfo: any;
|
}
|
|
const state = reactive<State>({
|
fullName: undefined,
|
hiprintTemplate: undefined,
|
hasActiveSliceConfig: false,
|
dataList: [],
|
loading: false,
|
labelLayout: normalizeLabelLayoutConfig(),
|
labelLayoutHtml: '',
|
layoutError: '',
|
mergeDataSet: '',
|
onPrint: undefined,
|
templateId: '',
|
systemInfo: {
|
printer: '',
|
printTime: '',
|
},
|
});
|
const { loading } = toRefs(state);
|
const userStore = useUserStore();
|
const { createMessage } = useMessage();
|
const [registerModal, { closeModal, changeLoading }] = useModalInner(init);
|
const { getFlowStateContent } = useFlowState();
|
const globSetting = useGlobSetting();
|
|
const getUserInfo: any = computed(() => userStore.getUserInfo || {});
|
const SHOW_LABEL_LAYOUT_ENTRY = false;
|
const MERGED_BATCH_PRINT_BODY_CLASS = 'jnpf-merged-batch-print';
|
const MERGED_BATCH_PRINT_STYLE_ID = 'jnpf-merged-batch-print-style';
|
let printTemplateJson: any;
|
let renderedTemplateHtml: any;
|
|
onUnmounted(() => setMergedBatchPrintMode(false));
|
watch(
|
() => state.labelLayout,
|
() => renderPreview(),
|
{ deep: true },
|
);
|
|
/**
|
* 初始化
|
* @param data
|
*/
|
async function init(data) {
|
const { id, formInfo = [], labelLayout, mergeDataSet = '', pageBottomSpacing, pageTopSpacing } = data || {};
|
state.fullName = '';
|
state.hiprintTemplate = undefined;
|
state.hasActiveSliceConfig = false;
|
state.dataList = [];
|
state.labelLayout = normalizeLabelLayoutConfig(labelLayout);
|
if (!SHOW_LABEL_LAYOUT_ENTRY) state.labelLayout.enabled = false;
|
state.labelLayoutHtml = '';
|
state.layoutError = '';
|
printTemplateJson = undefined;
|
renderedTemplateHtml = undefined;
|
state.onPrint = data?.onPrint;
|
state.templateId = id || '';
|
state.mergeDataSet = typeof mergeDataSet === 'string' ? mergeDataSet.trim() : '';
|
setMergedBatchPrintMode(!!state.mergeDataSet);
|
if (!id || !formInfo.length) return;
|
$('#previewDesignedWrap').html(null);
|
changeLoading(true);
|
|
state.loading = true;
|
const { data: resData } = (await getBatchData({ id, formInfo })) || {};
|
state.loading = false;
|
changeLoading(false);
|
if (!resData) return;
|
state.hasActiveSliceConfig = resData.some((item) => hasActiveSliceConfig(item?.globalConfig));
|
getSystemInfo();
|
const printDataArr = resData?.map((item, index) => {
|
let { printData, printTemplate, operatorRecordList = [], convertConfig = '' } = item || {};
|
try {
|
const targetTpl = applyMergedBatchPrintLayout(JSON.parse(printTemplate), {
|
mergeDataSet: state.mergeDataSet,
|
pageBottomSpacing,
|
pageTopSpacing,
|
});
|
if (index === 0) {
|
printTemplateJson = targetTpl;
|
const panel = targetTpl?.panels?.[0];
|
if (!(state.labelLayout.labelWidth > 0)) state.labelLayout.labelWidth = Number(panel?.width) || 0;
|
if (!(state.labelLayout.labelHeight > 0)) state.labelLayout.labelHeight = Number(panel?.height) || 0;
|
state.hiprintTemplate = new hiprint.PrintTemplate({ template: targetTpl });
|
state.fullName = item.fullName || '';
|
}
|
if (convertConfig) printData = handleConvert(printData, convertConfig);
|
printData.operatorRecordList = operatorRecordList.map((o) => ({
|
...o,
|
handleTime: dayjs(o.handleTime).format('YYYY-MM-DD HH:mm:ss'),
|
handleStatus: getFlowStateContent(o.handleStatus),
|
}));
|
printData.systemInfo = state.systemInfo;
|
return printData;
|
} catch {
|
$('#previewDesignedWrap').append('<div class="print-single-wrap"><div class="tpl-invalid">模板已失效,请重新设计</div></div>');
|
return null;
|
}
|
});
|
if (!state.hiprintTemplate) return;
|
state.dataList = mergePrintDataSet(printDataArr, mergeDataSet);
|
initHinnn();
|
renderedTemplateHtml = state.hiprintTemplate?.getHtml(state.dataList);
|
renderPreview();
|
}
|
|
function renderPreview() {
|
if (!renderedTemplateHtml || !state.hiprintTemplate) return;
|
state.layoutError = '';
|
if (!state.labelLayout.enabled) {
|
state.labelLayoutHtml = '';
|
$('#previewDesignedWrap').html(renderedTemplateHtml);
|
return;
|
}
|
if (state.mergeDataSet) return showLayoutError('标签合并打印不能与数据集合并打印同时启用');
|
if (state.hasActiveSliceConfig) return showLayoutError('标签合并打印不能与打印模板的多联打印设置同时启用');
|
try {
|
const result = buildLabelLayoutHtml(renderedTemplateHtml, printTemplateJson, state.labelLayout);
|
state.labelLayoutHtml = result.html;
|
$('#previewDesignedWrap').html(result.html);
|
} catch (error) {
|
showLayoutError(error instanceof Error ? error.message : '标签拼版失败,请检查打印配置');
|
}
|
}
|
/**
|
* 下载文件
|
*/
|
async function handleDownload() {
|
if (!state.hiprintTemplate || !state.dataList?.length) return;
|
const fileName = `${state.fullName}_${dayjs().format('YYYYMMDDHHmmss')}`;
|
state.loading = true;
|
try {
|
if (state.labelLayout.enabled) {
|
if (!state.labelLayoutHtml) return;
|
await exportLabelLayoutPdf({
|
fileName,
|
html: state.labelLayoutHtml,
|
paperHeight: state.labelLayout.paperHeight,
|
paperWidth: state.labelLayout.paperWidth,
|
});
|
return;
|
}
|
await state.hiprintTemplate.toPdf(state.dataList, fileName, { isDownload: true });
|
} catch (error) {
|
console.error('[PrintBrowse] 导出 PDF 失败:', error);
|
createMessage.error(error instanceof Error ? error.message : '导出 PDF 失败');
|
} finally {
|
state.loading = false;
|
}
|
}
|
/**
|
* 打印文件
|
*/
|
async function handlePrint() {
|
if (state.loading || !state.hiprintTemplate || !state.dataList?.length || (state.labelLayout.enabled && !state.labelLayoutHtml)) return;
|
state.loading = true;
|
let awaitingPrintResult = false;
|
try {
|
const printConfig: any = {
|
clientOptions: { title: state.fullName || '' },
|
onError: () => {
|
state.loading = false;
|
},
|
onPrint: () => {
|
createMessage.success('打印任务已发送至打印客户端');
|
try {
|
state.onPrint?.(state.dataList, { fullName: state.fullName || '', templateId: state.templateId });
|
} catch (error) {
|
console.error('[PrintBrowse] 打印回调执行失败:', error);
|
} finally {
|
state.loading = false;
|
}
|
},
|
styleHandler: () => {
|
const mergedTableStyle = state.mergeDataSet ? `<style>${getMergedBatchPrintTableStyle()}</style>` : '';
|
return `<link rel="stylesheet" href="/css/print-lock.css" />${mergedTableStyle}`;
|
},
|
};
|
if (state.labelLayout.enabled) {
|
printConfig.html = state.labelLayoutHtml;
|
} else {
|
printConfig.data = state.dataList;
|
printConfig.template = state.hiprintTemplate;
|
}
|
const result = await onlineUtils.hiprint(printConfig);
|
awaitingPrintResult = Boolean(result);
|
} catch (error) {
|
console.error('[PrintBrowse] 打印失败:', error);
|
} finally {
|
if (!awaitingPrintResult) state.loading = false;
|
}
|
}
|
|
function hasActiveSliceConfig(globalConfig: unknown) {
|
if (!globalConfig) return false;
|
try {
|
const config = typeof globalConfig === 'string' ? JSON.parse(globalConfig) : globalConfig;
|
return Array.isArray(config?.sliceConfig) && config.sliceConfig.some((item) => item?.dataSet && Number(item?.limit) > 0);
|
} catch {
|
return false;
|
}
|
}
|
|
function showLayoutError(message: string) {
|
state.labelLayoutHtml = '';
|
state.layoutError = message;
|
const error = $('<div class="label-layout-error"></div>').text(message);
|
$('#previewDesignedWrap').empty().append(error);
|
}
|
|
function setMergedBatchPrintMode(enabled: boolean) {
|
if (typeof document === 'undefined') return;
|
document.body.classList.toggle(MERGED_BATCH_PRINT_BODY_CLASS, enabled);
|
|
const existingStyle = document.getElementById(MERGED_BATCH_PRINT_STYLE_ID);
|
if (!enabled) {
|
existingStyle?.remove();
|
return;
|
}
|
if (existingStyle) return;
|
|
const style = document.createElement('style');
|
style.id = MERGED_BATCH_PRINT_STYLE_ID;
|
style.textContent = getMergedBatchPrintTableStyle(`body.${MERGED_BATCH_PRINT_BODY_CLASS}`);
|
document.head.append(style);
|
}
|
function handleConvert(data, convertConfig) {
|
const convertConfigList = JSON.parse(convertConfig);
|
for (const e of convertConfigList) {
|
if (e.type !== 'singleImg') continue;
|
const table = e.field.split('.')[0];
|
const field = e.field.split('.')[1];
|
if (!Reflect.has(data, table)) continue;
|
for (let j = 0; j < data[table].length; j++) {
|
if (Reflect.has(data[table][j], field) && data[table][j][field]) {
|
// 图片加前缀
|
data[table][j][field] = getImgUrl(data[table][j][field]);
|
}
|
}
|
}
|
return data;
|
}
|
// 获取系统信息
|
function getSystemInfo() {
|
const systemPrinter = `${unref(getUserInfo)?.userName}/${unref(getUserInfo)?.userAccount}`;
|
const systemPrintTime = dayjs(Date.now()).format('YYYY-MM-DD HH:mm:ss');
|
state.systemInfo.printer = systemPrinter;
|
state.systemInfo.printTime = systemPrintTime;
|
}
|
function getImgUrl(url) {
|
return getAuthMediaUrl(url, false);
|
}
|
// 重写hinnn
|
function initHinnn() {
|
if (!(window as any).hinnn) return;
|
(window as any).hinnn.apiUrl = globSetting.apiURL;
|
(window as any).hinnn.getAuthMediaUrl = getImgUrl;
|
(window as any).hinnn.dateFormat = function (date, format) {
|
if (!date) return '';
|
if (!Number.isNaN(date) && typeof date === 'string') date = Number(date);
|
format = format.replaceAll('y', 'Y').replaceAll('d', 'D');
|
return dayjs(date).format(format);
|
};
|
}
|
</script>
|
|
<template>
|
<BasicModal
|
v-bind="$attrs"
|
@register="registerModal"
|
default-fullscreen
|
:footer="null"
|
:closable="false"
|
:keyboard="false"
|
destroy-on-close
|
class="jnpf-full-modal full-modal jnpf-print-preview-modal">
|
<template #title>
|
<div class="jnpf-full-modal-header">
|
<div class="header-title">
|
<SystemBrandImage class="header-logo" />
|
<span class="header-dot"></span>
|
<p class="header-txt">打印预览</p>
|
</div>
|
<a-space class="options" :size="10">
|
<a-button type="primary" @click="handleDownload" :disabled="loading || (state.labelLayout.enabled && !state.labelLayoutHtml)">导出PDF</a-button>
|
<a-button type="primary" @click="handlePrint" :disabled="loading">{{ $t('common.printText') }}</a-button>
|
<a-button @click="closeModal()">{{ $t('common.cancelText') }}</a-button>
|
</a-space>
|
</div>
|
</template>
|
<div class="print-preview-layout">
|
<div id="previewDesignedWrap"></div>
|
<aside v-if="SHOW_LABEL_LAYOUT_ENTRY" class="label-layout-settings">
|
<div class="label-layout-settings-header">
|
<span>标签合并打印<BasicHelp text="将多条记录对应的完整标签排列到同一张物理纸上" /></span>
|
<a-switch v-model:checked="state.labelLayout.enabled" :disabled="loading" />
|
</div>
|
<template v-if="state.labelLayout.enabled">
|
<div class="label-layout-settings-grid">
|
<label>每页标签数<a-input-number v-model:value="state.labelLayout.itemsPerPage" :min="1" :precision="0" /></label>
|
<label>每行标签数<a-input-number v-model:value="state.labelLayout.columns" :min="1" :precision="0" /></label>
|
<label>标签宽度(mm)<a-input-number v-model:value="state.labelLayout.labelWidth" :min="1" :precision="2" /></label>
|
<label>标签高度(mm)<a-input-number v-model:value="state.labelLayout.labelHeight" :min="1" :precision="2" /></label>
|
<label>纸张宽度(mm)<a-input-number v-model:value="state.labelLayout.paperWidth" :min="1" :precision="2" /></label>
|
<label>纸张高度(mm)<a-input-number v-model:value="state.labelLayout.paperHeight" :min="1" :precision="2" /></label>
|
<label>起始留空数<a-input-number v-model:value="state.labelLayout.startOffset" :min="0" :precision="0" /></label>
|
<label>水平间距(mm)<a-input-number v-model:value="state.labelLayout.horizontalGap" :min="0" :precision="2" /></label>
|
<label>垂直间距(mm)<a-input-number v-model:value="state.labelLayout.verticalGap" :min="0" :precision="2" /></label>
|
<label>上边距(mm)<a-input-number v-model:value="state.labelLayout.marginTop" :min="0" :precision="2" /></label>
|
<label>右边距(mm)<a-input-number v-model:value="state.labelLayout.marginRight" :min="0" :precision="2" /></label>
|
<label>下边距(mm)<a-input-number v-model:value="state.labelLayout.marginBottom" :min="0" :precision="2" /></label>
|
<label>左边距(mm)<a-input-number v-model:value="state.labelLayout.marginLeft" :min="0" :precision="2" /></label>
|
</div>
|
<div v-if="state.layoutError" class="label-layout-settings-error">{{ state.layoutError }}</div>
|
</template>
|
</aside>
|
</div>
|
</BasicModal>
|
</template>
|
<style lang="scss">
|
#previewDesignedWrap {
|
flex: 1;
|
min-width: 0;
|
height: 100%;
|
overflow: hidden auto;
|
}
|
|
.print-preview-layout {
|
display: flex;
|
gap: 12px;
|
height: 100%;
|
min-height: 0;
|
}
|
|
.label-layout-settings {
|
flex: 0 0 320px;
|
align-self: flex-start;
|
max-height: 100%;
|
padding: 16px;
|
overflow: hidden auto;
|
background: var(--component-background);
|
border-left: 1px solid var(--border-color-base);
|
}
|
|
.label-layout-settings-header {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
min-height: 32px;
|
font-size: 16px;
|
font-weight: 600;
|
}
|
|
.label-layout-settings-grid {
|
display: grid;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
gap: 12px;
|
margin-top: 16px;
|
|
label {
|
display: flex;
|
flex-direction: column;
|
gap: 6px;
|
min-width: 0;
|
font-size: 13px;
|
color: var(--text-color-secondary);
|
}
|
|
.ant-input-number {
|
width: 100%;
|
}
|
}
|
|
.label-layout-settings-error {
|
padding: 8px;
|
margin-top: 12px;
|
font-size: 12px;
|
line-height: 1.5;
|
color: var(--error-color);
|
background: var(--error-color-outline);
|
border-radius: 4px;
|
}
|
|
.jnpf-full-modal.jnpf-print-preview-modal {
|
.ant-modal-body > .scrollbar {
|
padding: 0 !important;
|
}
|
|
.scrollbar__view {
|
overflow: hidden auto !important;
|
}
|
|
.hiprint-printPanel {
|
margin-bottom: 10px;
|
|
&:last-child {
|
margin-bottom: 0;
|
}
|
|
.hiprint-printPaper {
|
margin: 10px auto;
|
background-color: #fff;
|
}
|
}
|
|
.tpl-invalid {
|
box-sizing: border-box;
|
width: 210mm;
|
height: 296mm;
|
padding-top: 20px;
|
margin: 0 auto;
|
font-size: 16px;
|
text-align: center;
|
background: #fff;
|
}
|
|
.label-layout-error {
|
box-sizing: border-box;
|
width: 100%;
|
padding: 48px 24px;
|
font-size: 14px;
|
color: var(--text-color-secondary);
|
text-align: center;
|
}
|
}
|
|
#hiwprint_iframe {
|
border: 0 !important;
|
}
|
</style>
|