优化打印模板界面,更新打印机选择逻辑,支持PrintDot桥接打印,改进快速打印功能,增强用户体验。新增图标显示于报表节和组件框按钮,调整布局以提升可用性。

This commit is contained in:
geht
2026-04-17 20:30:07 +08:00
parent 93b2208675
commit ffb6f2bd0a
2 changed files with 171 additions and 349 deletions

View File

@@ -10,7 +10,7 @@
allow-clear
show-search
option-filter-prop="label"
placeholder="选择打印机(本地/网络)"
:placeholder="printerSelectPlaceholder"
/>
<a-input
v-model:value="manualPrinterName"
@@ -58,50 +58,35 @@
</BasicTable>
<PrintTemplateModal @register="registerModal" @success="handleSuccess" />
<NativeTemplateListPreviewModal v-model:open="nativeListPreviewOpen" :template-id="nativeListPreviewTemplateId" />
<a-modal v-model:open="quickPrintVisible" title="快速打印" width="820px" :confirm-loading="quickPrintLoading" @ok="handleQuickPrint">
<a-modal
v-model:open="quickNativePrintOpen"
title="快速打印"
width="480px"
:footer="null"
destroy-on-close
>
<a-space direction="vertical" style="width: 100%" size="middle">
<a-radio-group v-model:value="quickPrintMode">
<a-radio-button value="templateStyle">按模板样式打印推荐</a-radio-button>
<a-radio-button value="lodopTemplate">Lodop实验模板样式</a-radio-button>
<a-radio-button value="pdfServer">前端转PDF后端打印</a-radio-button>
<a-radio-button value="printDotBridge">PrintDot 本地桥接PDF</a-radio-button>
<a-radio-button value="serverText">服务端直打纯文本</a-radio-button>
</a-radio-group>
<div v-if="quickPrintMode === 'printDotBridge'" style="font-size: 12px; color: rgba(0, 0, 0, 0.55)">
需本机运行 PrintDot 客户端默认 WebSocket ws://127.0.0.1:1122/ws勾选PrintDot
桥接并刷新打印机后可选择桥接器上报的打印机支持原生模板与 hiprint 模板均在前端转 PDF 后送出
</div>
<a-space style="width: 100%">
<a-alert
type="info"
show-icon
message="请选择原生模板"
description="将打开与列表「预览」相同的弹窗,在其中编辑模板 JSON、参数 JSON并使用「浏览器打印」或 PrintDot「桥接器打印」。"
/>
<div>
<div class="skill-field-label">模板编号</div>
<a-select
v-model:value="quickPrintForm.templateCode"
v-model:value="quickNativeTemplateCode"
:options="templateCodeOptions"
style="width: 280px"
style="width: 100%"
show-search
option-filter-prop="label"
placeholder="选择模板编号"
/>
<a-select
v-model:value="quickPrintForm.printerName"
:options="printerOptions"
style="width: 320px"
show-search
allow-clear
option-filter-prop="label"
placeholder="选择打印机(可为空使用系统默认)"
/>
</a-space>
<a-textarea
v-model:value="quickPrintForm.dataJson"
:rows="12"
placeholder='传入打印数据JSON例如{"docNo":"MO-001","mainTable":[{"materialCode":"M01","qty":10}]}'
/>
<a-divider plain orientation="left" style="margin: 4px 0">预览</a-divider>
<div style="font-size: 12px; color: rgba(0, 0, 0, 0.55); line-height: 1.6">
打开打印设计器并自动执行与工具栏预览相同的逻辑同一套 hiprint 预览样式注入与表格合并后处理确保与在设计器内预览一致
</div>
<a-button type="button" :loading="quickPreviewLoading" @click.prevent.stop="handleQuickPrintDesignerPreview">
预览与设计器一致
</a-button>
<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>
@@ -189,7 +174,6 @@
deleteOne,
batchDelete,
queryPrinters,
directPrint,
directPrintPdf,
queryByCode,
queryById,
@@ -198,20 +182,12 @@
import { resolveProviders } from './hiprint/qhmesProvider';
import PrintTemplateModal from './components/PrintTemplateModal.vue';
import NativeTemplateListPreviewModal from './components/NativeTemplateListPreviewModal.vue';
import { QUICK_PRINT_PREVIEW_STORAGE_KEY } from './quickPrintPreviewStorage';
import {
buildPdfBase64FromHtmlFragment,
extractBodyInnerHtmlFromFullDocument,
} from './utils/printHtmlToPdfBase64';
import { buildPdfBase64FromHtmlFragment } from './utils/printHtmlToPdfBase64';
import {
fetchPrintDotPrinters,
getPrintDotBridgeConfig,
printDotSendPdf,
resolvePrintDotPrinterName,
setPrintDotBridgeConfig,
} from './utils/printDotBridge';
import { normalizeImportedNativeSchema } from './native/core/nativeSchemaNormalize';
import { renderNativePrintHtml } from './native/core/printRenderer';
import { PRINT_TEMPLATE_SELECTED_PRINTER_KEY } from './utils/printNativeViaPrintDot';
defineOptions({ name: 'PrintTemplateList' });
@@ -226,18 +202,10 @@
const selectedPrinterName = ref<string | undefined>();
const printerOptions = ref<Array<{ label: string; value: string }>>([]);
const manualPrinterName = ref('');
const quickPrintVisible = ref(false);
const quickPrintLoading = ref(false);
const quickPreviewLoading = ref(false);
const quickNativePrintOpen = ref(false);
const quickNativeTemplateCode = ref<string>('');
const quickNativePrintLoading = ref(false);
const templateCodeOptions = ref<Array<{ label: string; value: string }>>([]);
const quickPrintForm = ref<{ templateCode: string; printerName?: string; dataJson: string }>({
templateCode: '',
printerName: '__system_default__',
dataJson: '',
});
const quickPrintMode = ref<'templateStyle' | 'lodopTemplate' | 'pdfServer' | 'printDotBridge' | 'serverText'>(
'templateStyle',
);
const LS_PRINT_DOT_ENABLED = 'qhmes_print_dot_enabled';
const printDotEnabled = ref(localStorage.getItem(LS_PRINT_DOT_ENABLED) === '1');
const printDotCfg = getPrintDotBridgeConfig();
@@ -246,8 +214,16 @@
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);
@@ -338,6 +314,50 @@
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 : []),
@@ -346,9 +366,6 @@
.map((item) => String(item || '').trim())
.filter(Boolean)
.filter((item, index, arr) => arr.indexOf(item) === index);
const optionMap = new Map<string, { label: string; value: string }>();
// 永远保留“系统默认打印机”兜底项,避免插件不可用时无法选择
optionMap.set('__system_default__', { label: '系统默认打印机', value: '__system_default__' });
names.forEach((item) => {
optionMap.set(item, { label: item, value: item });
});
@@ -359,31 +376,10 @@
});
}
printerOptions.value = Array.from(optionMap.values());
if (printDotEnabled.value) {
try {
const dotList = await fetchPrintDotPrinters();
dotList.forEach((p) => {
const name = String(p.name || '').trim();
if (!name) {
return;
}
if (!optionMap.has(name)) {
optionMap.set(name, { label: `[PrintDot] ${name}`, value: name });
}
});
if (showMessage && dotList.length) {
createMessage.success(`PrintDot 已连接,识别 ${dotList.length} 台打印机`);
}
} catch (e: any) {
if (showMessage) {
createMessage.warning(`PrintDot${e?.message || '无法连接本地桥接器'}`);
}
}
}
if (showMessage) {
if (names.length) {
createMessage.success(`已从服务端识别到 ${names.length} 台打印机`);
} else if (!printDotEnabled.value) {
} else {
const reason = String(payload?.capability?.localReason || '').trim();
createMessage.warning(`服务端未返回可用打印机。${reason || '请在后端配置网络打印机后重试。'}`);
}
@@ -416,82 +412,50 @@
}
async function openQuickPrintModal() {
quickPrintForm.value.printerName = selectedPrinterName.value || '__system_default__';
quickPrintMode.value = 'templateStyle';
quickNativeTemplateCode.value = '';
if (!templateCodeOptions.value.length) {
await loadTemplateCodeOptions();
}
quickPrintVisible.value = true;
quickNativePrintOpen.value = true;
}
/**
* 跳转打印设计器并自动调用与设计器「预览」相同的 previewTemplate 逻辑(见 PrintDesigner runQuickPrintPreviewFromSessionStorage
*/
async function handleQuickPrintDesignerPreview() {
const templateCode = String(quickPrintForm.value.templateCode || '').trim();
/** 快速打印:仅支持原生模板,打开与列表相同的预览弹窗(浏览器打印 / PrintDot */
async function confirmQuickOpenNativePreview() {
const templateCode = String(quickNativeTemplateCode.value || '').trim();
if (!templateCode) {
createMessage.warning('请先选择模板编号');
return;
}
const dataText = String(quickPrintForm.value.dataJson || '').trim();
if (!dataText) {
createMessage.warning('请先输入打印数据 JSON');
return;
}
try {
JSON.parse(dataText);
} catch (error: any) {
createMessage.error(`打印数据JSON格式错误${error?.message || '未知错误'}`);
return;
}
quickPreviewLoading.value = true;
quickNativePrintLoading.value = true;
try {
const tpl = (await queryByCode(templateCode)) as Record<string, any>;
const id = String(tpl?.id ?? (tpl as any)?.result?.id ?? '').trim();
const row = (tpl?.result ?? tpl) as Record<string, any>;
const id = String(row?.id ?? tpl?.id ?? '').trim();
if (!id) {
createMessage.error(`未找到模板记录主键,无法打开设计器预览。返回字段:${Object.keys(tpl || {}).join(', ') || '空'}`);
createMessage.error('未找到模板记录,请确认编号是否正确');
return;
}
sessionStorage.setItem(
QUICK_PRINT_PREVIEW_STORAGE_KEY,
JSON.stringify({
dataJsonText: dataText,
}),
);
const previewQuery = {
id,
quickPrintPreview: '1',
_qpt: String(Date.now()),
} as Record<string, string>;
const navigateToDesignerPreview = async () => {
const isDuplicateNav = (err: unknown) => /redundant|duplicated|Avoided/i.test(String((err as any)?.message ?? err ?? ''));
const candidates = [
{ name: 'print-designer' as const, query: previewQuery },
{ path: '/print/designer', query: previewQuery },
];
let lastErr: unknown = null;
for (const loc of candidates) {
try {
await router.push(loc as any);
return;
} catch (e: any) {
lastErr = e;
if (isDuplicateNav(e)) {
await router.replace(loc as any);
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;
}
throw lastErr;
};
await navigateToDesignerPreview();
createMessage.success('正在打开设计器预览…');
quickPrintVisible.value = false;
}
if (engine !== 'native') {
createMessage.warning('快速打印仅支持「原生模板」engine 为 native请从列表预览 hiprint 模板或使用设计器');
return;
}
nativeListPreviewTemplateId.value = id;
nativeListPreviewOpen.value = true;
quickNativePrintOpen.value = false;
} catch (error: any) {
sessionStorage.removeItem(QUICK_PRINT_PREVIEW_STORAGE_KEY);
createMessage.error(`无法打开预览:${error?.message || '未知错误'}`);
createMessage.error(`加载模板失败:${error?.message || '未知错误'}`);
} finally {
quickPreviewLoading.value = false;
quickNativePrintLoading.value = false;
}
}
@@ -1318,191 +1282,6 @@
</html>`;
}
async function handleQuickPrint() {
const templateCode = String(quickPrintForm.value.templateCode || '').trim();
if (!templateCode) {
createMessage.warning('请选择模板编号');
return;
}
const dataText = String(quickPrintForm.value.dataJson || '').trim();
if (!dataText) {
createMessage.warning('请先输入打印数据JSON');
return;
}
let dataJson: any;
try {
dataJson = JSON.parse(dataText);
} catch (error: any) {
createMessage.error(`打印数据JSON格式错误${error?.message || '未知错误'}`);
return;
}
quickPrintLoading.value = true;
try {
const printer = String(quickPrintForm.value.printerName || '').trim();
if (quickPrintMode.value === 'serverText') {
await directPrint({
templateCode,
printerName: printer,
dataJson,
});
createMessage.success('已提交服务端直打任务');
} else if (quickPrintMode.value === 'printDotBridge') {
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;
}
let pdfBase64: string;
if (templateJson?.engine === 'native') {
const normalized = normalizeImportedNativeSchema(templateJson);
const pw = Number(tplData?.paperWidthMm);
const ph = Number(tplData?.paperHeightMm);
if (pw > 0) {
normalized.page.width = pw;
}
if (ph > 0) {
normalized.page.height = ph;
}
const fullHtml = await renderNativePrintHtml(normalized, dataJson);
const inner = extractBodyInnerHtmlFromFullDocument(fullHtml);
pdfBase64 = await buildPdfBase64FromHtmlFragment(
inner,
normalized.page.width,
normalized.page.height,
{ paginate: true },
);
} else {
await initHiprintForQuickPrint();
const runtimeTemplate = new hiprint.PrintTemplate({
template: templateJson,
});
const html = optimizeMergedHeaderHtml(await resolveTemplateHtml(runtimeTemplate, dataJson), templateJson);
if (!html) {
createMessage.error('未能生成预览 HTML无法转 PDF');
return;
}
const panel = Array.isArray(templateJson?.panels) && templateJson.panels.length ? templateJson.panels[0] : {};
const widthMm = Number(panel?.width || 210);
const heightMm = Number(panel?.height || 297);
pdfBase64 = await buildPdfBase64FromHtmlFragment(html, widthMm, heightMm);
}
const dotPrinters = await fetchPrintDotPrinters();
const resolvedPrinter = resolvePrintDotPrinterName(printer, dotPrinters);
if (!resolvedPrinter) {
createMessage.error('PrintDot请选择具体打印机或确认桥接器已返回打印机列表');
return;
}
const dotResult = await printDotSendPdf({
printer: resolvedPrinter,
pdfBase64,
jobName: templateCode,
timeoutMs: 180000,
});
if (!dotResult.ok) {
throw new Error(dotResult.message || 'PrintDot 打印失败');
}
createMessage.success('已通过 PrintDot 提交打印');
} else {
await initHiprintForQuickPrint();
if (quickPrintMode.value === 'pdfServer') {
await executePdfServerPrint({
templateCode,
printerName: printer,
dataJson,
fileName: `${templateCode}.pdf`,
});
createMessage.success('已提交PDF到后端打印');
} else {
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,
});
if (quickPrintMode.value === 'lodopTemplate') {
try {
await ensureClodopScriptLoaded();
} catch (e: any) {
createMessage.error(
`无法连接 C-Lodop${e?.message || '加载 CLodopfuncs.js 失败'}。请确认本机服务已启动;若站点为 HTTPS需安装扩展版并在浏览器中访问一次 https://localhost.lodop.net:8443 信任证书。`,
);
return;
}
const lodop =
(typeof (window as any).getLodop === 'function' ? (window as any).getLodop() : null) ||
(window as any)?.LODOP ||
(window as any)?.CLODOP;
if (!lodop) {
createMessage.error('未检测到 LODOP/C-Lodop请先安装并启动后重试');
return;
}
const html = optimizeMergedHeaderHtml(await resolveTemplateHtml(runtimeTemplate, dataJson), templateJson);
if (!html) {
const proto = Object.getPrototypeOf(runtimeTemplate);
const methodNames = Object.getOwnPropertyNames(proto || {}).filter((name) => name !== 'constructor');
console.warn('[Lodop实验] 未拿到模板HTMLPrintTemplate methods:', methodNames);
createMessage.error('当前 hiprint 版本未导出可用 HTML暂无法走 Lodop 实验模式(可先用“按模板样式打印”)');
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);
lodop.PRINT_INIT(`QH-MES-${templateCode}`);
if (widthMm > 0 && heightMm > 0 && typeof lodop.SET_PRINT_PAGESIZE === 'function') {
lodop.SET_PRINT_PAGESIZE(1, Math.round(widthMm * 10), Math.round(heightMm * 10), '');
}
if (printer && printer !== '__system_default__' && typeof lodop.SET_PRINTER_INDEXA === 'function') {
lodop.SET_PRINTER_INDEXA(printer);
}
const docHtml = buildLodopDocumentHtml(html, widthMm > 0 ? widthMm : undefined, heightMm > 0 ? heightMm : undefined);
const printWidth = widthMm > 0 ? `${widthMm}mm` : 'RightMargin:0mm';
lodop.ADD_PRINT_HTM('0mm', '0mm', printWidth, 'BottomMargin:0mm', docHtml);
if (typeof lodop.SET_PRINT_MODE === 'function') {
// 关闭强制满宽,尽量保持模板原始列宽比例
lodop.SET_PRINT_MODE('FULL_WIDTH_FOR_OVERFLOW', false);
}
lodop.PRINT();
createMessage.success('已通过 Lodop 提交打印任务(实验模式)');
} else {
if (printer && printer !== '__system_default__') {
try {
runtimeTemplate.print(dataJson, { printer });
} catch (_error) {
runtimeTemplate.print(dataJson, { printerName: printer });
}
} else {
runtimeTemplate.print(dataJson);
}
createMessage.success('已按模板样式发起打印');
}
}
}
quickPrintVisible.value = false;
} catch (error: any) {
createMessage.error(`打印失败:${error?.message || '未知错误'}`);
} finally {
quickPrintLoading.value = false;
}
}
function handleCreateNative() {
openModal(true, { isUpdate: false, isNative: true });
}

View File

@@ -14,16 +14,28 @@
<a-tabs v-model:activeKey="activeTab" size="small" class="palette-tabs">
<a-tab-pane key="bands" tab="报表节">
<div class="tab-scroll">
<a-button v-for="item in bandItems" :key="item.type" block size="small" class="palette-btn" @click="emit('add', item.type)">
{{ item.label }}
</a-button>
<div class="palette-item-grid">
<a-button v-for="item in bandItems" :key="item.type" size="small" class="palette-btn" @click="emit('add', item.type)">
<Icon :icon="item.icon" class="palette-btn-icon" />
<span class="palette-btn-label">{{ item.label }}</span>
</a-button>
</div>
</div>
</a-tab-pane>
<a-tab-pane key="components" tab="组件框">
<div class="tab-scroll">
<a-button v-for="item in componentItems" :key="item.type" block size="small" class="palette-btn" @click="emit('add', item.type)">
{{ item.label }}
</a-button>
<div class="palette-item-grid">
<a-button
v-for="item in componentItems"
:key="item.type"
size="small"
class="palette-btn"
@click="emit('add', item.type)"
>
<Icon :icon="item.icon" class="palette-btn-icon" />
<span class="palette-btn-label">{{ item.label }}</span>
</a-button>
</div>
</div>
</a-tab-pane>
<a-tab-pane key="params">
@@ -48,6 +60,7 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { Icon } from '/@/components/Icon';
import BindingDetailFieldsEditor from './BindingDetailFieldsEditor.vue';
import BindingParamsEditor from './BindingParamsEditor.vue';
import type {
@@ -89,22 +102,22 @@
function onUpdateDetailTables(detailTables: NativeDataBindingDetailTable[]) {
emit('update-data-binding', { detailTables });
}
const bandItems: Array<{ type: NativeElementType; label: string }> = [
{ type: 'reportHeader', label: '报表头' },
{ type: 'reportFooter', label: '报表尾' },
const bandItems: Array<{ type: NativeElementType; label: string; icon: string }> = [
{ type: 'reportHeader', label: '报表头', icon: 'ant-design:vertical-align-top-outlined' },
{ type: 'reportFooter', label: '报表尾', icon: 'ant-design:vertical-align-bottom-outlined' },
];
const componentItems: Array<{ type: NativeElementType; label: string }> = [
{ type: 'title', label: '标题' },
{ type: 'subtitle', label: '副标题' },
{ type: 'text', label: '文本' },
{ type: 'date', label: '日期' },
{ type: 'pageNo', label: '页码' },
{ type: 'image', label: '图片' },
{ type: 'table', label: '普通表格' },
{ type: 'detailTable', label: '明细表格' },
{ type: 'freeTable', label: '自由表格' },
{ type: 'qrcode', label: '二维码' },
{ type: 'barcode', label: '条形码' },
const componentItems: Array<{ type: NativeElementType; label: string; icon: string }> = [
{ type: 'title', label: '标题', icon: 'ant-design:font-size-outlined' },
{ type: 'subtitle', label: '副标题', icon: 'ant-design:align-left-outlined' },
{ type: 'text', label: '文本', icon: 'ant-design:file-text-outlined' },
{ type: 'date', label: '日期', icon: 'ant-design:calendar-outlined' },
{ type: 'pageNo', label: '页码', icon: 'ant-design:book-outlined' },
{ type: 'image', label: '图片', icon: 'ant-design:picture-outlined' },
{ type: 'table', label: '普通表格', icon: 'ant-design:table-outlined' },
{ type: 'detailTable', label: '明细表格', icon: 'ant-design:insert-row-below-outlined' },
{ type: 'freeTable', label: '自由表格', icon: 'ant-design:border-outlined' },
{ type: 'qrcode', label: '二维码', icon: 'mdi:qrcode' },
{ type: 'barcode', label: '条形码', icon: 'mdi:barcode' },
];
</script>
@@ -148,9 +161,39 @@
padding: 6px 10px 12px 8px;
}
/* 报表节 / 组件框:两列网格,图标 + 文案 */
.palette-item-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 6px 8px;
align-items: stretch;
}
.palette-btn {
margin-bottom: 6px;
margin-bottom: 0;
width: 100%;
height: auto;
min-height: 32px;
padding: 6px 8px;
display: inline-flex;
align-items: center;
justify-content: flex-start;
gap: 6px;
text-align: left;
line-height: 1.25;
white-space: normal;
}
.palette-btn-icon {
flex-shrink: 0;
font-size: 16px;
color: rgba(0, 0, 0, 0.65);
}
.palette-btn-label {
flex: 1;
min-width: 0;
word-break: break-all;
}
.palette-tab-help-label {