Files
qhmes/jeecgboot-vue3/src/views/print/template/index.vue

1486 lines
54 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div>
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<template #tableTitle>
<a-space>
<a-select
v-model:value="selectedPrinterName"
:options="printerOptions"
style="width: 260px"
allow-clear
show-search
option-filter-prop="label"
:placeholder="printerSelectPlaceholder"
/>
<a-input
v-model:value="manualPrinterName"
style="width: 180px"
placeholder="手动输入打印机名称"
@pressEnter="addManualPrinter"
/>
<a-button @click="addManualPrinter">添加打印机</a-button>
<a-button @click="refreshPrinterOptions">刷新打印机</a-button>
<a-checkbox v-model:checked="printDotEnabled" @change="onPrintDotEnabledChange">PrintDot 桥接</a-checkbox>
<a-input
v-model:value="printDotWsUrl"
style="width: 200px"
placeholder="WS 地址"
@blur="persistPrintDotConfig"
/>
<a-input-password
v-model:value="printDotKey"
style="width: 120px"
placeholder="密钥(可选)"
autocomplete="new-password"
@blur="persistPrintDotConfig"
/>
<a-button @click="downloadPrintPlugin">下载打印插件</a-button>
</a-space>
<a-button type="primary" ghost @click="handleCreateNative" v-auth="'print:template:add'">新增原生模板</a-button>
<a-button type="primary" @click="openQuickPrintModal" v-auth="'print:template:list'">快速打印</a-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete" v-auth="'print:template:delete'">
<Icon icon="ant-design:delete-outlined" />
删除
</a-menu-item>
</a-menu>
</template>
<a-button>
批量操作
<Icon icon="mdi:chevron-down" />
</a-button>
</a-dropdown>
</template>
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" />
</template>
</BasicTable>
<PrintTemplateModal @register="registerModal" @success="handleSuccess" />
<NativeTemplateListPreviewModal v-model:open="nativeListPreviewOpen" :template-id="nativeListPreviewTemplateId" />
<a-modal
v-model:open="quickNativePrintOpen"
title="快速打印"
width="480px"
:footer="null"
destroy-on-close
>
<a-space direction="vertical" style="width: 100%" size="middle">
<a-alert
type="info"
show-icon
message="请选择原生模板"
description="将打开与列表「预览」相同的弹窗,在其中编辑模板 JSON、参数 JSON并使用「浏览器打印」或 PrintDot「桥接器打印」。"
/>
<div>
<div class="skill-field-label">模板编号</div>
<a-select
v-model:value="quickNativeTemplateCode"
:options="templateCodeOptions"
style="width: 100%"
show-search
option-filter-prop="label"
placeholder="选择模板编号"
/>
</div>
<a-space style="width: 100%; justify-content: flex-end">
<a-button @click="quickNativePrintOpen = false">取消</a-button>
<a-button type="primary" :loading="quickNativePrintLoading" @click="confirmQuickOpenNativePreview">打开预览</a-button>
</a-space>
</a-space>
</a-modal>
<a-modal
v-model:open="skillConvertVisible"
:title="skillModalTitle"
width="960px"
:footer="null"
destroy-on-close
@cancel="resetSkillConvertState"
>
<a-space direction="vertical" style="width: 100%" size="middle">
<a-alert type="info" show-icon message="流程说明" :description="skillModalAlertDescription" />
<a-row :gutter="12">
<a-col :span="10">
<div class="skill-field-label">模板支持编号/名称搜索</div>
<a-input
v-model:value="skillTemplateSearch"
allow-clear
placeholder="输入关键字过滤模板列表"
style="margin-bottom: 8px"
/>
<a-select
v-model:value="skillConvertForm.templateCode"
:options="skillTemplateFilteredOptions"
style="width: 100%"
show-search
:filter-option="false"
option-filter-prop="label"
placeholder="请选择模板编号"
/>
</a-col>
<a-col :span="14">
<div class="skill-field-label">打印机</div>
<a-select
v-model:value="skillConvertForm.printerName"
:options="printerOptions"
style="width: 100%"
show-search
allow-clear
option-filter-prop="label"
placeholder="可为空使用系统默认打印机"
/>
</a-col>
</a-row>
<div>
<div class="skill-field-label">打印数据 JSON</div>
<a-textarea v-model:value="skillConvertForm.dataJson" :rows="10" :placeholder="skillDataJsonPlaceholder" />
<div v-if="skillJsonError" class="skill-json-error">{{ skillJsonError }}</div>
<a-space style="margin-top: 8px" wrap>
<a-button size="small" @click="handleSkillInsertExample">填入示例</a-button>
<a-button size="small" @click="handleSkillFormatDataJson">格式化 JSON</a-button>
<a-button size="small" type="primary" ghost @click="handleSkillValidateJson">校验 JSON</a-button>
<a-button size="small" :loading="skillPreviewLoading" @click="handleSkillPreview">生成预览</a-button>
<a-button size="small" type="primary" :loading="skillPrintLoading" @click="handleSkillSubmitPrint">提交后端 PDF 打印</a-button>
</a-space>
</div>
<div>
<div class="skill-field-label">预览 Lodop 包装一致的 HTML供确认表头合并与纸张</div>
<a-spin :spinning="skillPreviewLoading">
<div class="skill-preview-wrap">
<iframe v-if="skillPreviewSrcdoc" class="skill-preview-iframe" :title="skillPreviewIframeTitle" :srcdoc="skillPreviewSrcdoc" />
<a-empty v-else description="请先选择模板并点击「生成预览」" />
</div>
</a-spin>
</div>
</a-space>
</a-modal>
</div>
</template>
<script lang="ts" name="PrintTemplateList" setup>
import { computed, onMounted, ref, watch } from 'vue';
import 'vue-plugin-hiprint/dist/print-lock.css';
import { Icon } from '/@/components/Icon';
import { BasicTable, TableAction } from '/@/components/Table';
import { useModal } from '/@/components/Modal';
import { useListPage } from '/@/hooks/system/useListPage';
import { useMessage } from '/@/hooks/web/useMessage';
import { useRouter } from 'vue-router';
import { columns, searchFormSchema } from './printTemplate.data';
import {
list,
add,
deleteOne,
batchDelete,
queryPrinters,
directPrintPdf,
queryByCode,
queryById,
} from './printTemplate.api';
import { ensureClodopScriptLoaded } from './lodopLoader';
import { resolveProviders } from './hiprint/qhmesProvider';
import PrintTemplateModal from './components/PrintTemplateModal.vue';
import NativeTemplateListPreviewModal from './components/NativeTemplateListPreviewModal.vue';
import { buildPdfBase64FromHtmlFragment } from './utils/printHtmlToPdfBase64';
import {
fetchPrintDotPrinters,
getPrintDotBridgeConfig,
setPrintDotBridgeConfig,
} from './utils/printDotBridge';
import { PRINT_TEMPLATE_SELECTED_PRINTER_KEY } from './utils/printNativeViaPrintDot';
defineOptions({ name: 'PrintTemplateList' });
/** 原生模板列表「预览」弹窗 */
const nativeListPreviewOpen = ref(false);
const nativeListPreviewTemplateId = ref<string | null>(null);
const router = useRouter();
const { createMessage } = useMessage();
const [registerModal, { openModal }] = useModal();
const selectedPrinterName = ref<string | undefined>();
const printerOptions = ref<Array<{ label: string; value: string }>>([]);
const manualPrinterName = ref('');
const quickNativePrintOpen = ref(false);
const quickNativeTemplateCode = ref<string>('');
const quickNativePrintLoading = ref(false);
const templateCodeOptions = ref<Array<{ label: string; value: string }>>([]);
const LS_PRINT_DOT_ENABLED = 'qhmes_print_dot_enabled';
const printDotEnabled = ref(localStorage.getItem(LS_PRINT_DOT_ENABLED) === '1');
const printDotCfg = getPrintDotBridgeConfig();
const printDotWsUrl = ref(printDotCfg.wsUrl);
const printDotKey = ref(printDotCfg.key);
function persistPrintDotConfig() {
setPrintDotBridgeConfig(printDotWsUrl.value, printDotKey.value);
// 桥接开启时 WS/密钥变更后重新拉取桥接器打印机列表
if (printDotEnabled.value) {
void refreshPrinterOptions(false);
}
}
const printerSelectPlaceholder = computed(() =>
printDotEnabled.value ? '选择打印机PrintDot 桥接)' : '选择打印机(本地/网络)',
);
function onPrintDotEnabledChange() {
localStorage.setItem(LS_PRINT_DOT_ENABLED, printDotEnabled.value ? '1' : '0');
void refreshPrinterOptions(false);
}
/** 下载 PrintDot 桥接器(静态资源 public/print-plugin/XSL-PrintDot.exe */
function downloadPrintPlugin() {
const base = import.meta.env.BASE_URL || '/';
const normalizedBase = base.endsWith('/') ? base : `${base}/`;
const url = `${normalizedBase}print-plugin/XSL-PrintDot.exe`;
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'XSL-PrintDot.exe');
link.rel = 'noopener';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
/** 技能转换打印:示例数据(与快速打印占位说明一致) */
const SKILL_DATA_JSON_EXAMPLE = `{
"docNo": "MO-001",
"mainTable": [
{ "materialCode": "M01", "materialName": "示例物料", "qty": 10 }
]
}`;
const skillDataJsonPlaceholder = `请填写对象或数组形式的打印数据,例如:\n${SKILL_DATA_JSON_EXAMPLE}`;
const skillConvertVisible = ref(false);
const skillConvertForm = ref<{ templateCode: string; printerName?: string; dataJson: string }>({
templateCode: '',
printerName: '__system_default__',
dataJson: SKILL_DATA_JSON_EXAMPLE,
});
const skillTemplateSearch = ref('');
const skillPreviewSrcdoc = ref('');
const skillPreviewLoading = ref(false);
const skillPrintLoading = ref(false);
const skillJsonError = ref('');
/** convert原「技能转换打印」guide对齐仓库 Cursor 技能 hiprint-export-print */
const skillPrintModalMode = ref<'convert' | 'guide'>('convert');
const skillModalTitle = computed(() =>
skillPrintModalMode.value === 'guide'
? '技能指南打印hiprint-export-print'
: '技能转换打印hiprint → PDF → 后端队列)',
);
const skillModalAlertDescription = computed(() =>
skillPrintModalMode.value === 'guide'
? '与项目 Cursor 技能「hiprint-export-print」约定一致hiprint 模板 JSON + 打印数据 JSON → 校验 → 生成预览Lodop 同源 HTML 包装)→ html2canvas + jsPDF 生成 PDF → 调用后端 directPrintPdf。开发人员可查阅仓库 .cursor/skills/hiprint-export-print/SKILL.md 中的文件路径、自检项与常见问题。'
: '选择模板并填写打印数据 JSON → 校验 → 生成预览确认版式 → 提交为 PDF 由后端发送到所选打印机。',
);
const skillPreviewIframeTitle = computed(() =>
skillPrintModalMode.value === 'guide' ? '技能指南打印预览' : '技能转换预览',
);
const skillTemplateFilteredOptions = computed(() => {
const keyword = skillTemplateSearch.value.trim().toLowerCase();
const all = templateCodeOptions.value;
if (!keyword) {
return all;
}
return all.filter(
(item) =>
String(item.value || '')
.toLowerCase()
.includes(keyword) || String(item.label || '').toLowerCase().includes(keyword),
);
});
let hiprint: any = null;
function resolveHiprint(module: any) {
const defaultExport = module?.default || {};
hiprint = module?.hiprint || defaultExport?.hiprint || (window as any)?.hiprint;
return hiprint;
}
async function initHiprintForQuickPrint() {
if (hiprint) {
return;
}
const module = await import('vue-plugin-hiprint');
const hp = resolveHiprint(module);
if (!hp) {
throw new Error('未获取到 hiprint 实例');
}
hp.init({ providers: resolveProviders(module) });
}
const { tableContext } = useListPage({
tableProps: {
title: '打印模板',
api: list,
columns,
rowKey: 'id',
formConfig: { schemas: searchFormSchema },
actionColumn: { width: 300 },
},
});
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
async function refreshPrinterOptions(showMessage = true) {
const optionMap = new Map<string, { label: string; value: string }>();
// 保留「系统默认」:本地模式交给浏览器/队列;桥接模式由 resolvePrintDotPrinterName 映射为桥接器默认打印机
optionMap.set('__system_default__', { label: '系统默认打印机', value: '__system_default__' });
if (printDotEnabled.value) {
try {
const dotList = await fetchPrintDotPrinters();
dotList.forEach((p) => {
const name = String(p.name || '').trim();
if (!name) {
return;
}
const defMark = p.isDefault ? '(默认)' : '';
optionMap.set(name, { label: `${name}${defMark}`, value: name });
});
if (selectedPrinterName.value && !optionMap.has(selectedPrinterName.value)) {
optionMap.set(selectedPrinterName.value, {
label: `${selectedPrinterName.value}(手动)`,
value: selectedPrinterName.value,
});
}
printerOptions.value = Array.from(optionMap.values());
if (showMessage) {
if (dotList.length) {
createMessage.success(`已从 PrintDot 桥接识别 ${dotList.length} 台打印机`);
} else {
createMessage.warning('PrintDot 已连接但未返回打印机列表');
}
}
} catch (e: any) {
if (selectedPrinterName.value && !optionMap.has(selectedPrinterName.value)) {
optionMap.set(selectedPrinterName.value, {
label: `${selectedPrinterName.value}(手动)`,
value: selectedPrinterName.value,
});
}
printerOptions.value = Array.from(optionMap.values());
if (showMessage) {
createMessage.warning(`PrintDot${e?.message || '无法连接本地桥接器'}`);
}
}
return;
}
const payload = (await queryPrinters()) as Record<string, any>;
const names = [
...(Array.isArray(payload?.serverPrinters) ? payload.serverPrinters : []),
...(Array.isArray(payload?.networkPrinters) ? payload.networkPrinters : []),
]
.map((item) => String(item || '').trim())
.filter(Boolean)
.filter((item, index, arr) => arr.indexOf(item) === index);
names.forEach((item) => {
optionMap.set(item, { label: item, value: item });
});
if (selectedPrinterName.value && !optionMap.has(selectedPrinterName.value)) {
optionMap.set(selectedPrinterName.value, {
label: `${selectedPrinterName.value}(手动)`,
value: selectedPrinterName.value,
});
}
printerOptions.value = Array.from(optionMap.values());
if (showMessage) {
if (names.length) {
createMessage.success(`已从服务端识别到 ${names.length} 台打印机`);
} else {
const reason = String(payload?.capability?.localReason || '').trim();
createMessage.warning(`服务端未返回可用打印机。${reason || '请在后端配置网络打印机后重试。'}`);
}
}
}
function addManualPrinter() {
const name = String(manualPrinterName.value || '').trim();
if (!name) {
return;
}
const exists = printerOptions.value.some((item) => item.value === name);
if (!exists) {
printerOptions.value = [...printerOptions.value, { label: `${name}(手动)`, value: name }];
}
selectedPrinterName.value = name;
manualPrinterName.value = '';
createMessage.success('已添加手动打印机名称');
}
async function loadTemplateCodeOptions() {
const pageData = (await list({ pageNo: 1, pageSize: 500 })) as Record<string, any>;
const records = (pageData?.records || pageData?.result?.records || []) as Record<string, any>[];
templateCodeOptions.value = records
.map((item) => ({
value: String(item?.templateCode || '').trim(),
label: `${String(item?.templateCode || '').trim()} ${item?.templateName ? `- ${item.templateName}` : ''}`.trim(),
}))
.filter((item) => !!item.value);
}
async function openQuickPrintModal() {
quickNativeTemplateCode.value = '';
if (!templateCodeOptions.value.length) {
await loadTemplateCodeOptions();
}
quickNativePrintOpen.value = true;
}
/** 快速打印:仅支持原生模板,打开与列表相同的预览弹窗(浏览器打印 / PrintDot */
async function confirmQuickOpenNativePreview() {
const templateCode = String(quickNativeTemplateCode.value || '').trim();
if (!templateCode) {
createMessage.warning('请先选择模板编号');
return;
}
quickNativePrintLoading.value = true;
try {
const tpl = (await queryByCode(templateCode)) as Record<string, any>;
const row = (tpl?.result ?? tpl) as Record<string, any>;
const id = String(row?.id ?? tpl?.id ?? '').trim();
if (!id) {
createMessage.error('未找到该模板记录,请确认编号是否正确');
return;
}
const templateJsonText = String(row?.templateJson ?? tpl?.templateJson ?? '').trim();
let engine: string | undefined;
if (templateJsonText) {
try {
engine = JSON.parse(templateJsonText)?.engine;
} catch {
createMessage.error('模板 JSON 无法解析,无法判断是否为原生模板');
return;
}
}
if (engine !== 'native') {
createMessage.warning('快速打印仅支持「原生模板」engine 为 native请从列表预览 hiprint 模板或使用设计器');
return;
}
nativeListPreviewTemplateId.value = id;
nativeListPreviewOpen.value = true;
quickNativePrintOpen.value = false;
} catch (error: any) {
createMessage.error(`加载模板失败:${error?.message || '未知错误'}`);
} finally {
quickNativePrintLoading.value = false;
}
}
function resetSkillConvertState() {
skillPreviewSrcdoc.value = '';
skillJsonError.value = '';
skillTemplateSearch.value = '';
}
function handleSkillInsertExample() {
skillConvertForm.value.dataJson = SKILL_DATA_JSON_EXAMPLE;
skillJsonError.value = '';
createMessage.success('已填入示例 JSON');
}
function handleSkillFormatDataJson() {
const text = String(skillConvertForm.value.dataJson || '').trim();
if (!text) {
createMessage.warning('请先输入 JSON');
return;
}
try {
skillConvertForm.value.dataJson = JSON.stringify(JSON.parse(text), null, 2);
skillJsonError.value = '';
createMessage.success('已格式化');
} catch (error: any) {
skillJsonError.value = error?.message || 'JSON 格式错误';
createMessage.error(`无法格式化:${skillJsonError.value}`);
}
}
/** silent=true 时不弹出“格式正确”提示,供预览/打印前校验 */
function validateSkillDataJsonInternal(silent: boolean): boolean {
const text = String(skillConvertForm.value.dataJson || '').trim();
if (!text) {
skillJsonError.value = '内容为空';
createMessage.warning('请先输入打印数据 JSON');
return false;
}
try {
JSON.parse(text);
skillJsonError.value = '';
if (!silent) {
createMessage.success('JSON 格式正确');
}
return true;
} catch (error: any) {
skillJsonError.value = error?.message || 'JSON 解析失败';
createMessage.error(`校验失败:${skillJsonError.value}`);
return false;
}
}
function handleSkillValidateJson() {
validateSkillDataJsonInternal(false);
}
/** hiprint 渲染 HTML → html2canvas → jsPDF → 提交后端 directPrintPdf */
async function executePdfServerPrint(params: {
templateCode: string;
printerName?: string;
dataJson: any;
fileName?: string;
}) {
const templateCode = String(params.templateCode || '').trim();
await initHiprintForQuickPrint();
const tplData = (await queryByCode(templateCode)) as Record<string, any>;
const templateJsonText = String(tplData?.templateJson || '').trim();
if (!templateJsonText) {
throw new Error('模板JSON为空无法生成 PDF');
}
let templateJson: any;
try {
templateJson = JSON.parse(templateJsonText);
} catch (error: any) {
throw new Error(`模板JSON格式错误${error?.message || '未知错误'}`);
}
const runtimeTemplate = new hiprint.PrintTemplate({
template: templateJson,
});
const html = optimizeMergedHeaderHtml(await resolveTemplateHtml(runtimeTemplate, params.dataJson), templateJson);
if (!html) {
throw new Error('当前模板未生成可用预览内容,无法转 PDF');
}
const panel = Array.isArray(templateJson?.panels) && templateJson.panels.length ? templateJson.panels[0] : {};
const widthMm = Number(panel?.width || 210);
const heightMm = Number(panel?.height || 297);
const pdfBase64 = await buildPdfBase64FromHtmlFragment(html, widthMm, heightMm);
const printer = String(params.printerName || '').trim();
await directPrintPdf({
templateCode,
printerName: printer,
dataJson: params.dataJson,
pdfBase64,
fileName: params.fileName || `${templateCode}.pdf`,
});
}
async function handleSkillPreview() {
const templateCode = String(skillConvertForm.value.templateCode || '').trim();
if (!templateCode) {
createMessage.warning('请先选择模板');
return;
}
if (!validateSkillDataJsonInternal(true)) {
return;
}
let dataJson: any;
try {
dataJson = JSON.parse(String(skillConvertForm.value.dataJson || '').trim());
} catch {
return;
}
skillPreviewLoading.value = true;
try {
await initHiprintForQuickPrint();
const tplData = (await queryByCode(templateCode)) as Record<string, any>;
const templateJsonText = String(tplData?.templateJson || '').trim();
if (!templateJsonText) {
createMessage.error('模板JSON为空');
return;
}
let templateJson: any;
try {
templateJson = JSON.parse(templateJsonText);
} catch (error: any) {
createMessage.error(`模板JSON格式错误${error?.message || ''}`);
return;
}
const runtimeTemplate = new hiprint.PrintTemplate({
template: templateJson,
});
const html = optimizeMergedHeaderHtml(await resolveTemplateHtml(runtimeTemplate, dataJson), templateJson);
if (!html) {
createMessage.error('未能从 hiprint 生成预览 HTML请检查模板与数据字段是否匹配');
return;
}
const panel = Array.isArray(templateJson?.panels) && templateJson.panels.length ? templateJson.panels[0] : {};
const widthMm = Number(panel?.width || 0);
const heightMm = Number(panel?.height || 0);
skillPreviewSrcdoc.value = buildLodopDocumentHtml(html, widthMm > 0 ? widthMm : undefined, heightMm > 0 ? heightMm : undefined);
createMessage.success('预览已生成');
} catch (error: any) {
createMessage.error(`预览失败:${error?.message || '未知错误'}`);
} finally {
skillPreviewLoading.value = false;
}
}
async function handleSkillSubmitPrint() {
const templateCode = String(skillConvertForm.value.templateCode || '').trim();
if (!templateCode) {
createMessage.warning('请先选择模板');
return;
}
if (!validateSkillDataJsonInternal(true)) {
return;
}
let dataJson: any;
try {
dataJson = JSON.parse(String(skillConvertForm.value.dataJson || '').trim());
} catch {
return;
}
skillPrintLoading.value = true;
try {
await executePdfServerPrint({
templateCode,
printerName: skillConvertForm.value.printerName,
dataJson,
fileName:
skillPrintModalMode.value === 'guide' ? `${templateCode}-hiprint-export-print.pdf` : `${templateCode}-skill.pdf`,
});
createMessage.success('已提交 PDF 到后端打印队列');
skillConvertVisible.value = false;
} catch (error: any) {
createMessage.error(`提交失败:${error?.message || '未知错误'}`);
} finally {
skillPrintLoading.value = false;
}
}
function normalizeHtmlOutput(value: any): string {
if (!value) {
return '';
}
if (typeof value === 'string') {
return value.trim();
}
if (Array.isArray(value)) {
return value
.map((item) => normalizeHtmlOutput(item))
.filter(Boolean)
.join('');
}
if (value instanceof HTMLElement) {
return value.outerHTML || value.innerHTML || '';
}
if (value && typeof value === 'object') {
// 兼容 jQuery 对象:优先取第一个元素的 HTML
const first = (value as any)[0];
if (first instanceof HTMLElement) {
return first.outerHTML || first.innerHTML || '';
}
if (typeof (value as any).html === 'function') {
try {
const htmlValue = (value as any).html();
return typeof htmlValue === 'string' ? htmlValue.trim() : '';
} catch (_error) {
// ignore
}
}
if (typeof (value as any).outerHTML === 'string') {
return String((value as any).outerHTML).trim();
}
if (typeof (value as any).innerHTML === 'string') {
return String((value as any).innerHTML).trim();
}
}
return '';
}
async function resolveTemplateHtml(runtimeTemplate: any, dataJson: any): Promise<string> {
const buildDataCandidates = (rawData: any): any[] => {
const candidates: any[] = [rawData];
if (Array.isArray(rawData)) {
candidates.push({
detailList: rawData,
mainTable: rawData,
list: rawData,
items: rawData,
dataList: rawData,
});
return candidates;
}
if (rawData && typeof rawData === 'object') {
const objectData = rawData as Record<string, any>;
const firstArrayKey = Object.keys(objectData).find((key) => Array.isArray(objectData[key]));
if (firstArrayKey) {
const arr = objectData[firstArrayKey];
candidates.push({
...objectData,
detailList: objectData.detailList ?? arr,
mainTable: objectData.mainTable ?? arr,
list: objectData.list ?? arr,
items: objectData.items ?? arr,
dataList: objectData.dataList ?? arr,
});
}
}
return candidates;
};
const fixedCandidates = ['getHtml', 'toHtml', 'getHtmlByData'];
const dataCandidates = buildDataCandidates(dataJson);
for (const methodName of fixedCandidates) {
const fn = runtimeTemplate?.[methodName];
if (typeof fn !== 'function') {
continue;
}
for (const dataItem of dataCandidates) {
try {
const maybeValue = await Promise.resolve(fn.call(runtimeTemplate, dataItem));
const html = normalizeHtmlOutput(maybeValue);
if (html) {
return html;
}
} catch (_error) {
// ignore
}
}
}
// 兜底:扫描原型上的潜在导出方法(不同版本命名不一致)
const proto = Object.getPrototypeOf(runtimeTemplate);
const methodNames = Object.getOwnPropertyNames(proto || {})
.filter((name) => name !== 'constructor')
.filter((name) => /html|preview|content/i.test(name))
.filter((name) => !/^print$/i.test(name));
for (const methodName of methodNames) {
const fn = runtimeTemplate?.[methodName];
if (typeof fn !== 'function') {
continue;
}
for (const dataItem of dataCandidates) {
const argSets = [[dataItem], [dataItem, {}], []] as any[][];
for (const args of argSets) {
try {
const maybeValue = await Promise.resolve(fn.apply(runtimeTemplate, args));
const html = normalizeHtmlOutput(maybeValue);
if (html) {
return html;
}
} catch (_error) {
// ignore
}
}
}
}
return '';
}
function normalizeHeaderRowsFromTemplate(options: Record<string, any>): any[][] {
const candidates = [options?.headerColumns, options?.columns, options?.tableColumns];
for (const candidate of candidates) {
if (!Array.isArray(candidate) || !candidate.length) {
continue;
}
if (Array.isArray(candidate[0])) {
return candidate.map((row: any) => (Array.isArray(row) ? row : []));
}
return [candidate];
}
return [];
}
function resolveTemplateHeaderMeta(templateJson?: any): { rows: any[][]; options: Record<string, any> | null } {
const panels = Array.isArray(templateJson?.panels) ? templateJson.panels : [];
for (const panel of panels) {
const printElements = Array.isArray(panel?.printElements) ? panel.printElements : [];
for (const element of printElements) {
const options = element?.options || {};
const rows = normalizeHeaderRowsFromTemplate(options);
if (rows.length) {
return { rows, options };
}
}
}
return { rows: [], options: null };
}
/** 与 PrintDesigner 一致:兼容 {{field}} / item.xxx */
function normalizeBindField(raw: unknown) {
let field = String(raw ?? '').trim();
if (!field) {
return '';
}
const mustacheMatch = field.match(/^\{\{\s*([^}]+)\s*\}\}$/);
if (mustacheMatch?.[1]) {
field = mustacheMatch[1].trim();
}
field = field.replace(/^item\./i, '').trim();
return field;
}
/**
* 与 PrintDesigner.resolveBodyColumnsByHeaderRows 一致:按表头网格得到叶子列顺序(含无 field 的占位列,如行号)
*/
function resolveBodyColumnsByHeaderRows(headerRows: any[]): any[] {
if (!Array.isArray(headerRows) || !headerRows.length) {
return [];
}
const rowCount = headerRows.length;
let totalCols = 0;
headerRows.forEach((row) => {
const width = (Array.isArray(row) ? row : []).reduce(
(sum: number, cell: any) => sum + (Number(cell?.colspan || cell?.colSpan || 1) || 1),
0,
);
totalCols = Math.max(totalCols, width);
});
if (!totalCols) {
return [];
}
const grid: any[][] = Array.from({ length: rowCount }, () => Array.from({ length: totalCols }, () => null));
for (let r = 0; r < rowCount; r += 1) {
const row = Array.isArray(headerRows[r]) ? headerRows[r] : [];
let c = 0;
row.forEach((cell: any) => {
while (c < totalCols && grid[r][c]) {
c += 1;
}
const rowspan = Number(cell?.rowspan || cell?.rowSpan || 1) || 1;
const colspan = Number(cell?.colspan || cell?.colSpan || 1) || 1;
for (let rr = r; rr < Math.min(rowCount, r + rowspan); rr += 1) {
for (let cc = c; cc < Math.min(totalCols, c + colspan); cc += 1) {
grid[rr][cc] = cell;
}
}
c += colspan;
});
}
const result: any[] = [];
for (let c = 0; c < totalCols; c += 1) {
let hitCell: any = null;
for (let r = rowCount - 1; r >= 0; r -= 1) {
const cell = grid[r][c];
const field = cell?.field || cell?.dataIndex || cell?.key || cell?.name || '';
if (field) {
hitCell = cell;
break;
}
}
if (!hitCell) {
result.push({ field: '', title: '', align: '', halign: '', width: '', autoWrap: false });
continue;
}
const field = normalizeBindField(hitCell?.field || hitCell?.dataIndex || hitCell?.key || hitCell?.name || '');
const align = String(hitCell?.align || '');
const halign = String(hitCell?.halign || hitCell?.align || '');
const width = hitCell?.width || hitCell?.minWidth || '';
const autoWrap = Boolean(hitCell?.autoWrap ?? hitCell?.tableAutoWrap ?? hitCell?.wrap ?? hitCell?.wordWrap ?? false);
result.push({ field, title: hitCell?.title || '', align, halign, width, autoWrap });
}
return result;
}
function resolveBodyColumnsFromTemplateOptions(options: Record<string, any>): any[] {
const headerRows = normalizeHeaderRowsFromTemplate(options);
if (headerRows.length) {
return resolveBodyColumnsByHeaderRows(headerRows);
}
if (Array.isArray(options?.fields) && options.fields.length) {
return options.fields.map((item: any) =>
typeof item === 'string'
? { field: normalizeBindField(item), title: item, align: '', halign: '', width: '', autoWrap: false }
: {
field: normalizeBindField(item?.field || item?.dataIndex || item?.key || ''),
title: item?.title || item?.text || item?.label || '',
width: item?.width || item?.columnWidth || item?.w || '',
align: item?.align || item?.halign || '',
halign: item?.halign || item?.align || '',
autoWrap: item?.autoWrap ?? item?.tableAutoWrap ?? item?.wrap ?? item?.wordWrap,
},
);
}
return [];
}
function applyBodyColumnMetaToTable(table: HTMLTableElement, options: Record<string, any>) {
const tbodyRow = table.querySelector('tbody tr');
if (!tbodyRow) {
return;
}
const bodyCellCount = tbodyRow.querySelectorAll('td').length;
if (!bodyCellCount) {
return;
}
let bodyColumns = resolveBodyColumnsFromTemplateOptions(options);
if (!bodyColumns.length) {
return;
}
if (bodyColumns.length > bodyCellCount) {
bodyColumns = bodyColumns.slice(0, bodyCellCount);
} else if (bodyColumns.length < bodyCellCount) {
const pad = bodyCellCount - bodyColumns.length;
for (let i = 0; i < pad; i += 1) {
bodyColumns.push({ field: '', title: '', align: '', halign: '', width: '', autoWrap: false });
}
}
const tableWidthPx = Number(options?.width || 0);
const rawWidths = bodyColumns.map((colMeta) => {
const w = Number(colMeta?.width || 0);
return Number.isFinite(w) && w > 0 ? w : 0;
});
const definedWidths = rawWidths.filter((v) => v > 0);
const avgWidth = definedWidths.length
? definedWidths.reduce((sum, v) => sum + v, 0) / definedWidths.length
: tableWidthPx > 0
? tableWidthPx / Math.max(1, bodyColumns.length)
: 100;
const effectiveWidths = rawWidths.map((v) => (v > 0 ? v : avgWidth));
const totalEffectiveWidth = effectiveWidths.reduce((sum, v) => sum + v, 0);
table.style.tableLayout = 'fixed';
table.style.width = '100%';
const colgroup = document.createElement('colgroup');
effectiveWidths.forEach((w) => {
const col = document.createElement('col');
const pct = totalEffectiveWidth > 0 ? ((w / totalEffectiveWidth) * 100).toFixed(6) : '';
col.setAttribute('style', pct ? `width:${pct}%;` : '');
colgroup.appendChild(col);
});
const old = table.querySelector('colgroup');
if (old) {
old.replaceWith(colgroup);
} else if (table.firstChild) {
table.insertBefore(colgroup, table.firstChild);
} else {
table.appendChild(colgroup);
}
const bodyRows = Array.from(table.querySelectorAll('tbody tr'));
bodyRows.forEach((tr) => {
const cells = Array.from(tr.querySelectorAll('td')) as HTMLTableCellElement[];
cells.forEach((td, idx) => {
const col = bodyColumns[idx];
if (!col) {
return;
}
const wrap = Boolean(col?.autoWrap);
const align = String(col?.align || col?.halign || '').trim();
if (wrap) {
td.style.whiteSpace = 'normal';
td.style.wordBreak = 'break-word';
td.style.overflowWrap = 'break-word';
td.style.lineHeight = td.style.lineHeight || '1.2';
} else {
td.style.whiteSpace = 'nowrap';
td.style.wordBreak = 'normal';
td.style.overflowWrap = 'normal';
}
if (align) {
td.style.textAlign = align;
}
});
});
}
function resolveHeaderRowHeight(options: Record<string, any>): string {
const candidates = [
options?.headerRowHeight,
options?.tableHeaderRowHeight,
options?.thRowHeight,
options?.rowHeight,
];
for (const value of candidates) {
if (value === null || value === undefined || value === '') {
continue;
}
const text = String(value).trim();
if (!text) {
continue;
}
return /\d$/.test(text) ? `${text}px` : text;
}
return '';
}
function buildHeaderThStyle(cell: any, options: Record<string, any>, rowHeight: string): string {
const headerBg =
options?.headerBackground ||
options?.tableHeaderBackground ||
options?.headerBg ||
options?.thBackground ||
'';
const headerFontSize = options?.headerFontSize || options?.thFontSize || '';
const headerLineHeight = options?.headerLineHeight || options?.thLineHeight || '';
const headerFontWeight = options?.headerFontWeight || options?.thFontWeight || '600';
const headerFontFamily = options?.headerFontFamily || options?.thFontFamily || '';
const headerAlign = String(cell?.halign || cell?.align || options?.headerAlign || 'center');
const headerAutoWrap = Boolean(cell?.autoWrap ?? cell?.tableAutoWrap ?? cell?.wrap ?? cell?.wordWrap ?? false);
return [
'border:1px solid #333',
'padding:2px 4px',
`text-align:${headerAlign}`,
'vertical-align:middle',
headerAutoWrap ? 'white-space:normal' : 'white-space:nowrap',
headerAutoWrap ? 'word-break:break-all' : '',
headerAutoWrap ? 'overflow-wrap:anywhere' : '',
`font-weight:${headerFontWeight}`,
headerFontFamily ? `font-family:${headerFontFamily}` : '',
headerBg ? `background:${headerBg}` : '',
headerFontSize ? `font-size:${headerFontSize}` : '',
headerLineHeight ? `line-height:${headerLineHeight}` : '',
rowHeight ? `height:${rowHeight}` : '',
rowHeight ? `min-height:${rowHeight}` : '',
!headerLineHeight && rowHeight ? `line-height:${rowHeight}` : '',
'print-color-adjust:exact',
'-webkit-print-color-adjust:exact',
]
.filter(Boolean)
.join(';');
}
function optimizeMergedHeaderHtml(sourceHtml: string, templateJson?: any): string {
if (!sourceHtml || !sourceHtml.includes('<table')) {
return sourceHtml;
}
const wrapper = document.createElement('div');
wrapper.innerHTML = sourceHtml;
const templateHeaderMeta = resolveTemplateHeaderMeta(templateJson);
const tables = Array.from(wrapper.querySelectorAll('table'));
tables.forEach((table) => {
const buildTemplateHeader = () => {
if (!templateHeaderMeta.rows.length || !templateHeaderMeta.options) {
return null;
}
const rowHeight = resolveHeaderRowHeight(templateHeaderMeta.options);
const thead = document.createElement('thead');
templateHeaderMeta.rows.forEach((row) => {
const tr = document.createElement('tr');
if (rowHeight) {
tr.setAttribute('style', `height:${rowHeight};min-height:${rowHeight};`);
}
row.forEach((cell) => {
const th = document.createElement('th');
const rowspan = Math.max(1, Number(cell?.rowspan || cell?.rowSpan || 1));
const colspan = Math.max(1, Number(cell?.colspan || cell?.colSpan || 1));
if (rowspan > 1) {
th.rowSpan = rowspan;
}
if (colspan > 1) {
th.colSpan = colspan;
}
th.innerHTML = String(cell?.title || cell?.text || cell?.label || '').trim();
th.setAttribute('style', buildHeaderThStyle(cell, templateHeaderMeta.options as Record<string, any>, rowHeight));
tr.appendChild(th);
});
thead.appendChild(tr);
});
return thead;
};
const injectedThead = buildTemplateHeader();
if (injectedThead) {
const oldThead = table.querySelector('thead');
if (oldThead) {
oldThead.replaceWith(injectedThead);
} else {
const allRows = Array.from(table.querySelectorAll('tr'));
const maybeHeaderRows = allRows.slice(0, templateHeaderMeta.rows.length);
maybeHeaderRows.forEach((row) => row.remove());
if (table.firstChild) {
table.insertBefore(injectedThead, table.firstChild);
} else {
table.appendChild(injectedThead);
}
}
}
if (templateHeaderMeta.options) {
applyBodyColumnMetaToTable(table as HTMLTableElement, templateHeaderMeta.options as Record<string, any>);
}
const detectHeaderRows = () => {
const thead = table.querySelector('thead');
if (thead) {
const headRows = Array.from(thead.querySelectorAll('tr'));
if (headRows.length) {
return headRows;
}
}
const allRows = Array.from(table.querySelectorAll('tr'));
const result: HTMLTableRowElement[] = [];
for (const row of allRows) {
const cells = Array.from(row.querySelectorAll('th,td')) as HTMLTableCellElement[];
if (!cells.length) {
continue;
}
const firstText = String(cells[0]?.textContent || '')
.replace(/\u00a0/g, '')
.replace(/\s+/g, '')
.trim();
// 命中明细行(首列纯数字)即停止,前面都视为表头区域
if (/^\d+$/.test(firstText)) {
break;
}
result.push(row as HTMLTableRowElement);
if (result.length >= 6) {
break;
}
}
return result;
};
const rows = detectHeaderRows();
if (rows.length < 2) {
return;
}
const normalizeHeaderText = (cell: HTMLTableCellElement | null | undefined) =>
String(cell?.textContent || '')
.replace(/\u00a0/g, '')
.replace(/\s+/g, '')
.trim();
type HeaderCellInfo = { el: HTMLTableCellElement; row: number; col: number; rowSpan: number; colSpan: number };
const matrix: Array<Array<HeaderCellInfo | null>> = [];
rows.forEach((row, rowIdx) => {
matrix[rowIdx] = matrix[rowIdx] || [];
let colIdx = 0;
const cells = Array.from(row.querySelectorAll('th,td')) as HTMLTableCellElement[];
cells.forEach((cell) => {
while (matrix[rowIdx][colIdx]) {
colIdx += 1;
}
const rowSpan = Math.max(1, Number(cell.getAttribute('rowspan') || 1));
const colSpan = Math.max(1, Number(cell.getAttribute('colspan') || 1));
const info: HeaderCellInfo = { el: cell, row: rowIdx, col: colIdx, rowSpan, colSpan };
for (let r = rowIdx; r < rowIdx + rowSpan; r += 1) {
matrix[r] = matrix[r] || [];
for (let c = colIdx; c < colIdx + colSpan; c += 1) {
matrix[r][c] = info;
}
}
colIdx += colSpan;
});
});
// 处理“上一行应 rowspan但下一行是空占位或同名重复”的情况
for (let r = 0; r < rows.length - 1; r += 1) {
const currentRow = matrix[r] || [];
const nextRow = matrix[r + 1] || [];
for (let c = 0; c < currentRow.length; c += 1) {
const info = currentRow[c];
if (!info || info.row !== r || info.col !== c || info.rowSpan > 1) {
continue;
}
const currentText = normalizeHeaderText(info.el);
if (!currentText) {
continue;
}
const mergeCells: HTMLTableCellElement[] = [];
let canMergeDown = true;
for (let k = 0; k < info.colSpan; k += 1) {
const below = nextRow[c + k];
if (!below || below.row !== r + 1 || below.col !== c + k || below.colSpan > 1 || below.rowSpan > 1) {
canMergeDown = false;
break;
}
const belowText = normalizeHeaderText(below.el);
const isPlaceholder = !belowText || belowText === '-';
const isSameTitle = !!currentText && belowText === currentText;
if (!isPlaceholder && !isSameTitle) {
canMergeDown = false;
break;
}
mergeCells.push(below.el);
}
if (canMergeDown && mergeCells.length) {
const nextRowSpan = Math.max(1, Number(info.el.getAttribute('rowspan') || 1));
info.el.setAttribute('rowspan', String(nextRowSpan + 1));
info.el.classList.add('qh-merged-header-cell');
info.el.style.borderBottom = 'none';
mergeCells.forEach((el) => {
el.style.borderTop = 'none';
el.remove();
});
}
}
}
});
return wrapper.innerHTML;
}
function buildLodopDocumentHtml(html: string, widthMm?: number, heightMm?: number): string {
const styleTexts = '';
// 仅保留 hiprint 打印相关样式,避免把业务页面全局样式带入后污染列宽计算
const stylesheetLinks = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
.map((node) => (node as HTMLLinkElement).href)
.filter(Boolean)
.filter((href) => /print-lock|hiprint/i.test(href))
.map((href) => `<link rel="stylesheet" href="${href}" />`)
.join('\n');
const pageCss =
widthMm && heightMm
? `@page { size: ${widthMm}mm ${heightMm}mm; margin: 0; }`
: '@page { margin: 0; }';
const rootSizeCss =
widthMm && heightMm
? `.lodop-print-root { width: ${widthMm}mm; margin: 0; padding: 0; overflow: visible; }`
: '.lodop-print-root { margin: 0; padding: 0; overflow: visible; }';
const compatCss = `
html, body { margin: 0; padding: 0; background: #fff; }
${rootSizeCss}
.hiprint-printTemplate {
transform: none !important;
transform-origin: top left !important;
zoom: 1 !important;
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
/* 打印态去掉设计器预览缩放,避免整体变小与合并表头被裁切 */
.hiprint-printTemplate [style*="transform: scale"] {
transform: none !important;
}
.hiprint-printTemplate table {
border-collapse: collapse !important;
border-spacing: 0 !important;
}
.hiprint-printTemplate th,
.hiprint-printTemplate td {
border-color: #333 !important;
border-style: solid !important;
border-width: 1px !important;
overflow: visible !important;
text-overflow: clip !important;
white-space: normal !important;
vertical-align: middle !important;
word-break: normal !important;
line-height: 1.35 !important;
}
.hiprint-printTemplate thead th {
line-height: 1.45 !important;
padding-top: 2px !important;
padding-bottom: 2px !important;
}
.hiprint-printTemplate .qh-merged-header-cell {
border-bottom: none !important;
}
.hiprint-printTemplate .hiprint-printElement,
.hiprint-printTemplate .hiprint-printElement > div,
.hiprint-printTemplate .hiprint-printElement span,
.hiprint-printTemplate .hiprint-printElement p {
overflow: visible !important;
text-overflow: clip !important;
}
/* 合并单元格专门处理:避免跨列跨行时边框与文字被裁切 */
.hiprint-printTemplate th[colspan], .hiprint-printTemplate td[colspan],
.hiprint-printTemplate th[rowspan], .hiprint-printTemplate td[rowspan] {
overflow: visible !important;
white-space: normal !important;
text-align: center !important;
vertical-align: middle !important;
position: relative;
z-index: 1;
background-clip: padding-box !important;
}
.hiprint-printTemplate tr {
page-break-inside: avoid !important;
}
.hiprint-printTemplate img, .hiprint-printTemplate canvas {
max-width: none !important;
max-height: none !important;
}
`;
return `<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
${stylesheetLinks}
<style>${styleTexts}</style>
<style>${pageCss}\n${compatCss}</style>
</head>
<body><div class="lodop-print-root">${html}</div></body>
</html>`;
}
function handleCreateNative() {
openModal(true, { isUpdate: false, isNative: true });
}
function handleEdit(record: Recordable) {
openModal(true, { isUpdate: true, record });
}
function isNativeTemplate(record: Recordable) {
const raw = record?.templateJson;
if (!raw) {
return false;
}
if (typeof raw === 'object') {
return raw?.engine === 'native';
}
if (typeof raw !== 'string') {
return false;
}
try {
const parsed = JSON.parse(raw);
return parsed?.engine === 'native';
} catch (_error) {
return false;
}
}
function handleDesign(record: Recordable) {
const path = isNativeTemplate(record) ? '/print/native-designer' : '/print/designer';
router.push({ path, query: { id: record.id } });
}
function handleNativeListPreview(record: Recordable) {
nativeListPreviewTemplateId.value = String(record.id || '');
nativeListPreviewOpen.value = true;
}
async function handleDelete(record: Recordable) {
await deleteOne({ id: record.id }, reload);
}
/** 复制为一条新模板(新编码 + 名称后缀,内容与原模板一致) */
async function handleCopy(record: Recordable) {
const id = String(record?.id || '').trim();
if (!id) {
createMessage.warning('无法复制:缺少模板主键');
return;
}
try {
const full = (await queryById(id)) as Record<string, any>;
const baseCode = String(full?.templateCode ?? record?.templateCode ?? 'TPL').trim() || 'TPL';
const ts = Date.now();
let templateCode = `${baseCode}_CP_${ts}`;
if (templateCode.length > 64) {
templateCode = `CP_${ts}`;
}
const baseName = String(full?.templateName ?? record?.templateName ?? baseCode).trim() || '模板';
const templateName = `${baseName}_副本`;
await add({
templateCode,
templateName,
category: String(full?.category ?? record?.category ?? 'form'),
paperWidthMm: full?.paperWidthMm ?? record?.paperWidthMm,
paperHeightMm: full?.paperHeightMm ?? record?.paperHeightMm,
paperOrientation: String(full?.paperOrientation ?? record?.paperOrientation ?? 'portrait'),
templateJson: String(full?.templateJson ?? record?.templateJson ?? '{}'),
remark: full?.remark != null ? String(full.remark) : record?.remark != null ? String(record.remark) : '',
});
createMessage.success('已复制为新模板');
await reload();
} catch (e: any) {
createMessage.error(e?.message || '复制失败');
}
}
async function batchHandleDelete() {
await batchDelete({ ids: selectedRowKeys.value.join(',') }, reload);
}
async function handleSuccess(payload?: Recordable) {
reload();
if (payload?.isNative !== true || payload?.isUpdate === true) {
return;
}
const savedId =
payload?.savedResult?.id ||
payload?.savedResult?.result?.id ||
payload?.values?.id;
if (savedId) {
router.push({ path: '/print/native-designer', query: { id: String(savedId) } });
return;
}
const templateCode = String(payload?.values?.templateCode || '').trim();
if (!templateCode) {
return;
}
try {
const record = (await queryByCode(templateCode)) as Record<string, any>;
if (record?.id) {
router.push({ path: '/print/native-designer', query: { id: String(record.id) } });
}
} catch (_error) {
// ignore
}
}
function getTableAction(record: Recordable) {
const isNative = isNativeTemplate(record);
const actions: any[] = [
{ label: isNative ? '原生设计' : 'Hiprint设计', onClick: handleDesign.bind(null, record), auth: 'print:template:edit' },
];
if (isNative) {
actions.push({ label: '预览', onClick: handleNativeListPreview.bind(null, record), auth: 'print:template:list' });
}
actions.push(
{ label: '编辑', onClick: handleEdit.bind(null, record), auth: 'print:template:edit' },
{ label: '复制', onClick: handleCopy.bind(null, record), auth: 'print:template:add' },
{
label: '删除',
color: 'error',
popConfirm: {
title: '确认删除?',
confirm: handleDelete.bind(null, record),
},
auth: 'print:template:delete',
},
);
return actions;
}
watch(
selectedPrinterName,
(value) => {
if (!value) {
localStorage.removeItem(PRINT_TEMPLATE_SELECTED_PRINTER_KEY);
return;
}
localStorage.setItem(PRINT_TEMPLATE_SELECTED_PRINTER_KEY, value);
},
{ immediate: false },
);
onMounted(async () => {
try {
await initHiprintForQuickPrint();
} catch (_error) {
// ignore
}
selectedPrinterName.value = localStorage.getItem(PRINT_TEMPLATE_SELECTED_PRINTER_KEY) || '__system_default__';
await refreshPrinterOptions(false);
if (!selectedPrinterName.value) {
selectedPrinterName.value = '__system_default__';
}
});
</script>
<style scoped lang="less">
.skill-field-label {
margin-bottom: 6px;
font-size: 13px;
color: rgba(0, 0, 0, 0.65);
}
.skill-json-error {
margin-top: 6px;
color: #cf1322;
font-size: 12px;
}
.skill-preview-wrap {
min-height: 360px;
max-height: 520px;
overflow: auto;
border: 1px solid #f0f0f0;
border-radius: 4px;
background: #fafafa;
}
.skill-preview-iframe {
width: 100%;
min-height: 360px;
height: 480px;
border: none;
background: #fff;
}
</style>