From 1aee848c6a258db2a21516a130ac1baddaaeb86c Mon Sep 17 00:00:00 2001 From: geht Date: Wed, 8 Apr 2026 17:40:52 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96PrintDesigner=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=EF=BC=8C=E6=96=B0=E5=A2=9E=E5=88=97=E7=9A=84=E9=A1=BA?= =?UTF-8?q?=E5=BA=8F=E5=B1=9E=E6=80=A7=EF=BC=8C=E8=B0=83=E6=95=B4=E5=88=97?= =?UTF-8?q?=E6=8E=92=E5=BA=8F=E9=80=BB=E8=BE=91=EF=BC=8C=E7=A1=AE=E4=BF=9D?= =?UTF-8?q?=E5=88=97=E5=9C=A8=E6=89=93=E5=8D=B0=E9=A2=84=E8=A7=88=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=E9=A1=BA=E5=BA=8F=E4=B8=8E=E8=AE=BE=E8=AE=A1=E4=B8=80?= =?UTF-8?q?=E8=87=B4=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../views/print/template/PrintDesigner.vue | 176 ++++++++++++++---- .../print/template/hiprint/qhmesProvider.ts | 19 +- 2 files changed, 148 insertions(+), 47 deletions(-) diff --git a/jeecgboot-vue3/src/views/print/template/PrintDesigner.vue b/jeecgboot-vue3/src/views/print/template/PrintDesigner.vue index cb4932a..6a268c0 100644 --- a/jeecgboot-vue3/src/views/print/template/PrintDesigner.vue +++ b/jeecgboot-vue3/src/views/print/template/PrintDesigner.vue @@ -61,7 +61,7 @@ @@ -254,6 +254,7 @@ } type SimpleColumn = { + order?: number; title: string; field: string; width?: number; @@ -261,11 +262,24 @@ }; function normalizeColumns(cols: any[]): any[] { - return cols.map((c) => ({ - title: c.title || c.field || '列', - field: c.field || '', - width: Number(c.width || 60), - align: c.align || 'left', + const normalized = (Array.isArray(cols) ? cols : []).map((c, idx) => { + const orderNum = Number(c?.order); + return { + _idx: idx, + _order: Number.isFinite(orderNum) ? orderNum : idx, + title: c?.title || c?.field || '列', + field: c?.field || '', + width: Number(c?.width || 60), + align: c?.align || 'left', + }; + }); + normalized.sort((a, b) => a._order - b._order || a._idx - b._idx); + return normalized.map((c, idx) => ({ + title: c.title, + order: idx, + field: c.field, + width: c.width, + align: c.align, colspan: 1, rowspan: 1, })); @@ -274,6 +288,7 @@ function compactColumnsForEditor(cols: any[]) { return normalizeColumns(cols).map((c) => ({ title: c.title, + order: c.order, field: c.field, width: c.width, align: c.align, @@ -296,10 +311,17 @@ function(t,e,printData){ var list = printData && Array.isArray(printData[opts.field || 'table']) ? printData[opts.field || 'table'] : []; var globalCols = printData && Array.isArray(printData.__qhmesTableColumns) ? printData.__qhmesTableColumns : []; var columns = Array.isArray(opts.columns) && opts.columns.length ? opts.columns : (globalCols.length ? globalCols : [ - { title: '物料', field: 'name', width: 90, align: 'left' }, - { title: '数量', field: 'qty', width: 45, align: 'right' }, - { title: '金额', field: 'amount', width: 45, align: 'right' } + { title: '物料', order: 0, field: 'name', width: 90, align: 'left' }, + { title: '数量', order: 1, field: 'qty', width: 45, align: 'right' }, + { title: '金额', order: 2, field: 'amount', width: 45, align: 'right' } ]); + columns = columns.slice().sort(function(a,b){ + var ao = Number(a && a.order); + var bo = Number(b && b.order); + var av = isFinite(ao) ? ao : 9999; + var bv = isFinite(bo) ? bo : 9999; + return av - bv; + }); var globalGroups = printData && Array.isArray(printData.__qhmesGroupFields) ? printData.__qhmesGroupFields : []; var groupFields = Array.isArray(opts.groupFields) && opts.groupFields.length ? opts.groupFields : globalGroups; var style = opts.__qhmesStyle || {}; @@ -502,9 +524,11 @@ function(t,e,printData){ } const mergedCols = normalizeColumns(parsedCols).map((c) => ({ title: c.title, + order: c.order, field: c.field, width: c.width, align: c.align, + headerBg: (c as any).headerBg || '', })); const mergedGroups = groupEnabled.value ? [...groupFields.value] : []; let changedCount = 0; @@ -649,6 +673,7 @@ function(t,e,printData){ } const mergedCols = normalizeColumns(parsedCols).map((c) => ({ title: c.title, + order: c.order, field: c.field, width: c.width, align: c.align, @@ -656,18 +681,30 @@ function(t,e,printData){ const mergedGroups = groupEnabled.value ? [...groupFields.value] : []; const readCanvasTableStyle = () => { try { - const tables = Array.from(document.querySelectorAll('#hiprint-printTemplate table')); + const tables = Array.from(document.querySelectorAll('#hiprint-printTemplate table')) as HTMLTableElement[]; if (!tables.length) return null; - const tb = tables[0] as HTMLTableElement; + // 选择“最像目标明细表”的 table:表头列最多、且可见 + const scoreTable = (tb: HTMLTableElement) => { + const thCount = tb.querySelectorAll('thead th').length; + const hasBody = tb.querySelectorAll('tbody tr').length > 0 ? 5 : 0; + const visible = (tb as HTMLElement).offsetParent ? 3 : 0; + return thCount * 10 + hasBody + visible; + }; + const tb = [...tables].sort((a, b) => scoreTable(b) - scoreTable(a))[0]; const ths = Array.from(tb.querySelectorAll('thead th')) as HTMLElement[]; const th = ths.length > 0 ? ths[0] : null; + const trHead = tb.querySelector('thead tr') as HTMLElement | null; const td = tb.querySelector('tbody td') as HTMLElement | null; const ref = th || td; if (!ref) return null; const cs = window.getComputedStyle(ref); const tbcs = window.getComputedStyle(tb); const validColor = (c: string) => !!c && c !== 'rgba(0, 0, 0, 0)' && c !== 'transparent'; - const headerColors = ths.map((cell) => window.getComputedStyle(cell).backgroundColor || ''); + const trHeadBg = trHead ? window.getComputedStyle(trHead).backgroundColor || '' : ''; + const headerColors = ths.map((cell) => { + const c = window.getComputedStyle(cell).backgroundColor || ''; + return validColor(c) ? c : trHeadBg; + }); const firstValidHeaderBg = headerColors.find((c) => validColor(c)) || ''; const bg = firstValidHeaderBg || (th ? window.getComputedStyle(th).backgroundColor : ''); const headerTitles = ths.map((cell) => (cell.textContent || '').trim()); @@ -753,32 +790,29 @@ function(t,e,printData){ }; // 保真模式:优先用画布真实样式兜底(含表头填充色) if (preserveDesignStyle.value && domStyle) { - // 保真模式:列顺序与表头标题优先按画布 DOM 对齐 - if (Array.isArray(domStyle.headerTitles) && domStyle.headerTitles.length > 0) { - const srcCols = Array.isArray(node.options.columns) ? [...node.options.columns] : []; - const unused = [...srcCols]; - const ordered = domStyle.headerTitles.map((title: string, idx: number) => { - let hitIdx = -1; - if (title) { - hitIdx = unused.findIndex((c: any) => (c?.title || '').trim() === title); - } - let base: any; - if (hitIdx >= 0) { - base = unused.splice(hitIdx, 1)[0]; - } else if (unused.length > 0) { - base = unused.shift(); - } else { - base = srcCols[idx] || mergedCols[idx] || { title: title || `列${idx + 1}`, field: '' }; - } - return { - ...base, - title: title || base?.title || base?.field || `列${idx + 1}`, - headerBg: Array.isArray(domStyle.headerColors) ? domStyle.headerColors[idx] || '' : '', - }; - }); - if (ordered.length > 0) { - node.options.columns = ordered; + // 保真模式:预览列顺序优先沿用画布组件本身的字段顺序,避免被 JSON 列配置重排 + // 仅当画布拿不到列定义时,才回退到列配置 JSON。 + const baseCols = originColumns.length > 0 ? originColumns : mergedCols; + if (baseCols.length > 0) { + let orderedCols = [...baseCols]; + // 进一步按画布可见表头文本重排,保证预览列顺序与画布视觉顺序一致 + if (Array.isArray(domStyle.headerTitles) && domStyle.headerTitles.length === baseCols.length) { + const used = new Set(); + orderedCols = domStyle.headerTitles.map((ht: string, idx: number) => { + const hitIdx = baseCols.findIndex( + (c: any, i: number) => !used.has(i) && String(c?.title || '').trim() === String(ht || '').trim() + ); + if (hitIdx >= 0) { + used.add(hitIdx); + return { ...baseCols[hitIdx], title: ht || baseCols[hitIdx].title }; + } + return { ...baseCols[idx], title: ht || baseCols[idx].title }; + }); } + node.options.columns = orderedCols.map((c: any, idx: number) => ({ + ...c, + headerBg: Array.isArray(domStyle.headerColors) ? domStyle.headerColors[idx] || (c as any).headerBg || '' : (c as any).headerBg || '', + })); } node.options.__qhmesStyle = { fontSize: node.options.__qhmesStyle.fontSize || domStyle.fontSize, @@ -829,6 +863,7 @@ function(t,e,printData){ type.options = type.options || {}; type.options.columns = mergedCols.map((c) => ({ title: c.title, + order: c.order, field: c.field, width: c.width, align: c.align, @@ -989,7 +1024,45 @@ function(t,e,printData){ } }; - const keys = Object.keys(firstRow); + // 字段顺序优先使用“画布可见表头顺序”映射,其次用画布明细列顺序,最后回退 JSON 键顺序 + const canvasFieldOrder = + existingCols + .map((c: any) => c?.field) + .filter((f: any) => !!f && Object.prototype.hasOwnProperty.call(firstRow, f)) || []; + const domHeaderTitles = (() => { + try { + const tables = Array.from(document.querySelectorAll('#hiprint-printTemplate table')); + if (!tables.length) return [] as string[]; + // 与预览逻辑一致:选表头列最多的表,减少抓错表的概率 + const scoreTable = (tb: Element) => tb.querySelectorAll('thead th').length * 10 + tb.querySelectorAll('tbody tr').length; + const tb = [...tables].sort((a, b) => scoreTable(b) - scoreTable(a))[0]; + return Array.from(tb.querySelectorAll('thead th')).map((th) => (th.textContent || '').trim()); + } catch { + return [] as string[]; + } + })(); + let keys = canvasFieldOrder.length > 0 ? [...canvasFieldOrder] : Object.keys(firstRow); + // 当 DOM 表头数量与画布列一致时,按表头标题重排字段顺序(避免 existingCols 顺序异常) + if (domHeaderTitles.length > 0 && existingCols.length === domHeaderTitles.length) { + const used = new Set(); + const byDom = domHeaderTitles + .map((ht) => { + const idx = existingCols.findIndex( + (c: any, i: number) => !used.has(i) && String(c?.title || '').trim() === String(ht || '').trim() + ); + if (idx < 0) return ''; + used.add(idx); + return existingCols[idx]?.field || ''; + }) + .filter((f: string) => !!f && Object.prototype.hasOwnProperty.call(firstRow, f)); + if (byDom.length > 0) { + keys = byDom; + } + } + // 补齐画布中没有但数据里有的字段 + Object.keys(firstRow).forEach((k) => { + if (!keys.includes(k)) keys.push(k); + }); const allEnglishLike = keys.every((k) => { const t = titleMap.get(k); return !t || t === k; @@ -1027,12 +1100,20 @@ function(t,e,printData){ price: '单价', total: '合计', }; - const cols = keys.map((k) => ({ + const cols = keys.map((k, idx) => ({ title: titleMap.get(k) || zhFallback[k.toLowerCase()] || k, + order: idx, field: k, width: 60, align: typeof firstRow[k] === 'number' ? 'right' : 'left', + headerBg: '', })); + // 若画布表头数量一致,则进一步按表头文本覆盖 title(以画布视觉为准) + if (domHeaderTitles.length === cols.length) { + cols.forEach((c, idx) => { + if (domHeaderTitles[idx]) c.title = domHeaderTitles[idx]; + }); + } tableColumnsJson.value = JSON.stringify(cols, null, 2); createMessage.success('已根据 table[0] 推导列配置'); } @@ -1152,6 +1233,19 @@ function(t,e,printData){ } function handlePreview() { + const printExt = { + styleHandler: () => { + return ` +`; + }, + }; if (!hiprintTemplate) { return; } @@ -1173,10 +1267,10 @@ function(t,e,printData){ const previewTemplate = buildPreviewTemplateWithGrouping(); if (previewTemplate) { const previewPrintTpl = new (hiprint as any).PrintTemplate({ template: previewTemplate }); - previewPrintTpl.print(data, {}, { callback: () => {} }); + previewPrintTpl.print(data, {}, { ...printExt, callback: () => {} }); return; } - hiprintTemplate.print(data, {}, { callback: () => {} }); + hiprintTemplate.print(data, {}, { ...printExt, callback: () => {} }); } watch( diff --git a/jeecgboot-vue3/src/views/print/template/hiprint/qhmesProvider.ts b/jeecgboot-vue3/src/views/print/template/hiprint/qhmesProvider.ts index 738452a..ad54491 100644 --- a/jeecgboot-vue3/src/views/print/template/hiprint/qhmesProvider.ts +++ b/jeecgboot-vue3/src/views/print/template/hiprint/qhmesProvider.ts @@ -75,9 +75,9 @@ export function createQhmesProvider() { height: 60, __qhmesManaged: true, columns: [ - { title: '物料', field: 'name', width: 90, align: 'left' }, - { title: '数量', field: 'qty', width: 45, align: 'right' }, - { title: '金额', field: 'amount', width: 45, align: 'right' }, + { title: '物料', order: 0, field: 'name', width: 90, align: 'left' }, + { title: '数量', order: 1, field: 'qty', width: 45, align: 'right' }, + { title: '金额', order: 2, field: 'amount', width: 45, align: 'right' }, ], groupFields: [], formatter: ` @@ -86,10 +86,17 @@ function(t,e,printData){ var list = printData && Array.isArray(printData[opts.field || 'table']) ? printData[opts.field || 'table'] : []; var globalCols = printData && Array.isArray(printData.__qhmesTableColumns) ? printData.__qhmesTableColumns : []; var columns = Array.isArray(opts.columns) && opts.columns.length ? opts.columns : (globalCols.length ? globalCols : [ - { title: '物料', field: 'name', width: 90, align: 'left' }, - { title: '数量', field: 'qty', width: 45, align: 'right' }, - { title: '金额', field: 'amount', width: 45, align: 'right' } + { title: '物料', order: 0, field: 'name', width: 90, align: 'left' }, + { title: '数量', order: 1, field: 'qty', width: 45, align: 'right' }, + { title: '金额', order: 2, field: 'amount', width: 45, align: 'right' } ]); + columns = columns.slice().sort(function(a,b){ + var ao = Number(a && a.order); + var bo = Number(b && b.order); + var av = isFinite(ao) ? ao : 9999; + var bv = isFinite(bo) ? bo : 9999; + return av - bv; + }); var globalGroups = printData && Array.isArray(printData.__qhmesGroupFields) ? printData.__qhmesGroupFields : []; var groupFields = Array.isArray(opts.groupFields) && opts.groupFields.length ? opts.groupFields : globalGroups; var style = opts.__qhmesStyle || {};