优化PrintDesigner组件,新增列的顺序属性,调整列排序逻辑,确保列在打印预览中的顺序与设计一致。

This commit is contained in:
geht
2026-04-08 17:40:52 +08:00
parent adc0631569
commit 1aee848c6a
2 changed files with 148 additions and 47 deletions

View File

@@ -61,7 +61,7 @@
<a-textarea <a-textarea
v-model:value="tableColumnsJson" v-model:value="tableColumnsJson"
:rows="6" :rows="6"
placeholder='例如:[{"title":"物料","field":"name","width":90},{"title":"数量","field":"qty","width":45}]' placeholder='例如:[{"order":0,"title":"物料","field":"name","width":90},{"order":1,"title":"数量","field":"qty","width":45}]'
@focus="onColumnsEditorFocus" @focus="onColumnsEditorFocus"
@blur="onColumnsEditorBlur" @blur="onColumnsEditorBlur"
/> />
@@ -254,6 +254,7 @@
} }
type SimpleColumn = { type SimpleColumn = {
order?: number;
title: string; title: string;
field: string; field: string;
width?: number; width?: number;
@@ -261,11 +262,24 @@
}; };
function normalizeColumns(cols: any[]): any[] { function normalizeColumns(cols: any[]): any[] {
return cols.map((c) => ({ const normalized = (Array.isArray(cols) ? cols : []).map((c, idx) => {
title: c.title || c.field || '列', const orderNum = Number(c?.order);
field: c.field || '', return {
width: Number(c.width || 60), _idx: idx,
align: c.align || 'left', _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, colspan: 1,
rowspan: 1, rowspan: 1,
})); }));
@@ -274,6 +288,7 @@
function compactColumnsForEditor(cols: any[]) { function compactColumnsForEditor(cols: any[]) {
return normalizeColumns(cols).map((c) => ({ return normalizeColumns(cols).map((c) => ({
title: c.title, title: c.title,
order: c.order,
field: c.field, field: c.field,
width: c.width, width: c.width,
align: c.align, 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 list = printData && Array.isArray(printData[opts.field || 'table']) ? printData[opts.field || 'table'] : [];
var globalCols = printData && Array.isArray(printData.__qhmesTableColumns) ? printData.__qhmesTableColumns : []; var globalCols = printData && Array.isArray(printData.__qhmesTableColumns) ? printData.__qhmesTableColumns : [];
var columns = Array.isArray(opts.columns) && opts.columns.length ? opts.columns : (globalCols.length ? globalCols : [ var columns = Array.isArray(opts.columns) && opts.columns.length ? opts.columns : (globalCols.length ? globalCols : [
{ title: '物料', field: 'name', width: 90, align: 'left' }, { title: '物料', order: 0, field: 'name', width: 90, align: 'left' },
{ title: '数量', field: 'qty', width: 45, align: 'right' }, { title: '数量', order: 1, field: 'qty', width: 45, align: 'right' },
{ title: '金额', field: 'amount', 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 globalGroups = printData && Array.isArray(printData.__qhmesGroupFields) ? printData.__qhmesGroupFields : [];
var groupFields = Array.isArray(opts.groupFields) && opts.groupFields.length ? opts.groupFields : globalGroups; var groupFields = Array.isArray(opts.groupFields) && opts.groupFields.length ? opts.groupFields : globalGroups;
var style = opts.__qhmesStyle || {}; var style = opts.__qhmesStyle || {};
@@ -502,9 +524,11 @@ function(t,e,printData){
} }
const mergedCols = normalizeColumns(parsedCols).map((c) => ({ const mergedCols = normalizeColumns(parsedCols).map((c) => ({
title: c.title, title: c.title,
order: c.order,
field: c.field, field: c.field,
width: c.width, width: c.width,
align: c.align, align: c.align,
headerBg: (c as any).headerBg || '',
})); }));
const mergedGroups = groupEnabled.value ? [...groupFields.value] : []; const mergedGroups = groupEnabled.value ? [...groupFields.value] : [];
let changedCount = 0; let changedCount = 0;
@@ -649,6 +673,7 @@ function(t,e,printData){
} }
const mergedCols = normalizeColumns(parsedCols).map((c) => ({ const mergedCols = normalizeColumns(parsedCols).map((c) => ({
title: c.title, title: c.title,
order: c.order,
field: c.field, field: c.field,
width: c.width, width: c.width,
align: c.align, align: c.align,
@@ -656,18 +681,30 @@ function(t,e,printData){
const mergedGroups = groupEnabled.value ? [...groupFields.value] : []; const mergedGroups = groupEnabled.value ? [...groupFields.value] : [];
const readCanvasTableStyle = () => { const readCanvasTableStyle = () => {
try { 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; 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 ths = Array.from(tb.querySelectorAll('thead th')) as HTMLElement[];
const th = ths.length > 0 ? ths[0] : null; 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 td = tb.querySelector('tbody td') as HTMLElement | null;
const ref = th || td; const ref = th || td;
if (!ref) return null; if (!ref) return null;
const cs = window.getComputedStyle(ref); const cs = window.getComputedStyle(ref);
const tbcs = window.getComputedStyle(tb); const tbcs = window.getComputedStyle(tb);
const validColor = (c: string) => !!c && c !== 'rgba(0, 0, 0, 0)' && c !== 'transparent'; 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 firstValidHeaderBg = headerColors.find((c) => validColor(c)) || '';
const bg = firstValidHeaderBg || (th ? window.getComputedStyle(th).backgroundColor : ''); const bg = firstValidHeaderBg || (th ? window.getComputedStyle(th).backgroundColor : '');
const headerTitles = ths.map((cell) => (cell.textContent || '').trim()); const headerTitles = ths.map((cell) => (cell.textContent || '').trim());
@@ -753,32 +790,29 @@ function(t,e,printData){
}; };
// 保真模式:优先用画布真实样式兜底(含表头填充色) // 保真模式:优先用画布真实样式兜底(含表头填充色)
if (preserveDesignStyle.value && domStyle) { if (preserveDesignStyle.value && domStyle) {
// 保真模式:列顺序与表头标题优先按画布 DOM 对齐 // 保真模式:预览列顺序优先沿用画布组件本身的字段顺序,避免被 JSON 列配置重排
if (Array.isArray(domStyle.headerTitles) && domStyle.headerTitles.length > 0) { // 仅当画布拿不到列定义时,才回退到列配置 JSON。
const srcCols = Array.isArray(node.options.columns) ? [...node.options.columns] : []; const baseCols = originColumns.length > 0 ? originColumns : mergedCols;
const unused = [...srcCols]; if (baseCols.length > 0) {
const ordered = domStyle.headerTitles.map((title: string, idx: number) => { let orderedCols = [...baseCols];
let hitIdx = -1; // 进一步按画布可见表头文本重排,保证预览列顺序与画布视觉顺序一致
if (title) { if (Array.isArray(domStyle.headerTitles) && domStyle.headerTitles.length === baseCols.length) {
hitIdx = unused.findIndex((c: any) => (c?.title || '').trim() === title); const used = new Set<number>();
} orderedCols = domStyle.headerTitles.map((ht: string, idx: number) => {
let base: any; const hitIdx = baseCols.findIndex(
if (hitIdx >= 0) { (c: any, i: number) => !used.has(i) && String(c?.title || '').trim() === String(ht || '').trim()
base = unused.splice(hitIdx, 1)[0]; );
} else if (unused.length > 0) { if (hitIdx >= 0) {
base = unused.shift(); used.add(hitIdx);
} else { return { ...baseCols[hitIdx], title: ht || baseCols[hitIdx].title };
base = srcCols[idx] || mergedCols[idx] || { title: title || `${idx + 1}`, field: '' }; }
} return { ...baseCols[idx], title: ht || baseCols[idx].title };
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;
} }
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 = { node.options.__qhmesStyle = {
fontSize: node.options.__qhmesStyle.fontSize || domStyle.fontSize, fontSize: node.options.__qhmesStyle.fontSize || domStyle.fontSize,
@@ -829,6 +863,7 @@ function(t,e,printData){
type.options = type.options || {}; type.options = type.options || {};
type.options.columns = mergedCols.map((c) => ({ type.options.columns = mergedCols.map((c) => ({
title: c.title, title: c.title,
order: c.order,
field: c.field, field: c.field,
width: c.width, width: c.width,
align: c.align, 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<number>();
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 allEnglishLike = keys.every((k) => {
const t = titleMap.get(k); const t = titleMap.get(k);
return !t || t === k; return !t || t === k;
@@ -1027,12 +1100,20 @@ function(t,e,printData){
price: '单价', price: '单价',
total: '合计', total: '合计',
}; };
const cols = keys.map((k) => ({ const cols = keys.map((k, idx) => ({
title: titleMap.get(k) || zhFallback[k.toLowerCase()] || k, title: titleMap.get(k) || zhFallback[k.toLowerCase()] || k,
order: idx,
field: k, field: k,
width: 60, width: 60,
align: typeof firstRow[k] === 'number' ? 'right' : 'left', 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); tableColumnsJson.value = JSON.stringify(cols, null, 2);
createMessage.success('已根据 table[0] 推导列配置'); createMessage.success('已根据 table[0] 推导列配置');
} }
@@ -1152,6 +1233,19 @@ function(t,e,printData){
} }
function handlePreview() { function handlePreview() {
const printExt = {
styleHandler: () => {
return `
<style>
@media print {
* {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
}
</style>`;
},
};
if (!hiprintTemplate) { if (!hiprintTemplate) {
return; return;
} }
@@ -1173,10 +1267,10 @@ function(t,e,printData){
const previewTemplate = buildPreviewTemplateWithGrouping(); const previewTemplate = buildPreviewTemplateWithGrouping();
if (previewTemplate) { if (previewTemplate) {
const previewPrintTpl = new (hiprint as any).PrintTemplate({ template: previewTemplate }); const previewPrintTpl = new (hiprint as any).PrintTemplate({ template: previewTemplate });
previewPrintTpl.print(data, {}, { callback: () => {} }); previewPrintTpl.print(data, {}, { ...printExt, callback: () => {} });
return; return;
} }
hiprintTemplate.print(data, {}, { callback: () => {} }); hiprintTemplate.print(data, {}, { ...printExt, callback: () => {} });
} }
watch( watch(

View File

@@ -75,9 +75,9 @@ export function createQhmesProvider() {
height: 60, height: 60,
__qhmesManaged: true, __qhmesManaged: true,
columns: [ columns: [
{ title: '物料', field: 'name', width: 90, align: 'left' }, { title: '物料', order: 0, field: 'name', width: 90, align: 'left' },
{ title: '数量', field: 'qty', width: 45, align: 'right' }, { title: '数量', order: 1, field: 'qty', width: 45, align: 'right' },
{ title: '金额', field: 'amount', width: 45, align: 'right' }, { title: '金额', order: 2, field: 'amount', width: 45, align: 'right' },
], ],
groupFields: [], groupFields: [],
formatter: ` formatter: `
@@ -86,10 +86,17 @@ function(t,e,printData){
var list = printData && Array.isArray(printData[opts.field || 'table']) ? printData[opts.field || 'table'] : []; var list = printData && Array.isArray(printData[opts.field || 'table']) ? printData[opts.field || 'table'] : [];
var globalCols = printData && Array.isArray(printData.__qhmesTableColumns) ? printData.__qhmesTableColumns : []; var globalCols = printData && Array.isArray(printData.__qhmesTableColumns) ? printData.__qhmesTableColumns : [];
var columns = Array.isArray(opts.columns) && opts.columns.length ? opts.columns : (globalCols.length ? globalCols : [ var columns = Array.isArray(opts.columns) && opts.columns.length ? opts.columns : (globalCols.length ? globalCols : [
{ title: '物料', field: 'name', width: 90, align: 'left' }, { title: '物料', order: 0, field: 'name', width: 90, align: 'left' },
{ title: '数量', field: 'qty', width: 45, align: 'right' }, { title: '数量', order: 1, field: 'qty', width: 45, align: 'right' },
{ title: '金额', field: 'amount', 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 globalGroups = printData && Array.isArray(printData.__qhmesGroupFields) ? printData.__qhmesGroupFields : [];
var groupFields = Array.isArray(opts.groupFields) && opts.groupFields.length ? opts.groupFields : globalGroups; var groupFields = Array.isArray(opts.groupFields) && opts.groupFields.length ? opts.groupFields : globalGroups;
var style = opts.__qhmesStyle || {}; var style = opts.__qhmesStyle || {};