增强PrintDesigner组件,新增单行表头表格类型,优化列提取和排序逻辑,确保打印预览与设计一致性。
This commit is contained in:
@@ -31,6 +31,8 @@
|
|||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
<a-collapse-panel key="detail" header="明细表" forceRender>
|
<a-collapse-panel key="detail" header="明细表" forceRender>
|
||||||
<div class="ep-draggable-item" tid="qhmesModule.tableSimple">普通明细表(默认列)</div>
|
<div class="ep-draggable-item" tid="qhmesModule.tableSimple">普通明细表(默认列)</div>
|
||||||
|
<div class="ep-draggable-item" tid="qhmesModule.tableSingleHeader">单行表头表格</div>
|
||||||
|
<div class="ep-draggable-item" tid="defaultModule.table">原生表格(官方)</div>
|
||||||
<div class="help-tip">默认普通表头;字段/绑定在右侧属性里修改。</div>
|
<div class="help-tip">默认普通表头;字段/绑定在右侧属性里修改。</div>
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
<a-collapse-panel key="section" header="报表区块(布局)" forceRender>
|
<a-collapse-panel key="section" header="报表区块(布局)" forceRender>
|
||||||
@@ -271,6 +273,7 @@
|
|||||||
field: c?.field || '',
|
field: c?.field || '',
|
||||||
width: Number(c?.width || 60),
|
width: Number(c?.width || 60),
|
||||||
align: c?.align || 'left',
|
align: c?.align || 'left',
|
||||||
|
headerBg: c?.headerBg || '',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
normalized.sort((a, b) => a._order - b._order || a._idx - b._idx);
|
normalized.sort((a, b) => a._order - b._order || a._idx - b._idx);
|
||||||
@@ -280,6 +283,7 @@
|
|||||||
field: c.field,
|
field: c.field,
|
||||||
width: c.width,
|
width: c.width,
|
||||||
align: c.align,
|
align: c.align,
|
||||||
|
headerBg: c.headerBg,
|
||||||
colspan: 1,
|
colspan: 1,
|
||||||
rowspan: 1,
|
rowspan: 1,
|
||||||
}));
|
}));
|
||||||
@@ -407,14 +411,26 @@ function(t,e,printData){
|
|||||||
return json || null;
|
return json || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mapColumnsKeepSequence(rawCols: any[]) {
|
||||||
|
if (!Array.isArray(rawCols)) return [];
|
||||||
|
return rawCols.map((c, idx) => ({
|
||||||
|
title: c?.title || c?.field || '列',
|
||||||
|
order: Number.isFinite(Number(c?.order)) ? Number(c.order) : idx,
|
||||||
|
field: c?.field || '',
|
||||||
|
width: Number(c?.width || 60),
|
||||||
|
align: c?.align || 'left',
|
||||||
|
headerBg: c?.headerBg || '',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
function extractColumnsFromElement(el: any) {
|
function extractColumnsFromElement(el: any) {
|
||||||
if (Array.isArray(el?.options?.columns) && el.options.columns.length) {
|
if (Array.isArray(el?.options?.columns) && el.options.columns.length) {
|
||||||
return compactColumnsForEditor(el.options.columns);
|
return mapColumnsKeepSequence(el.options.columns);
|
||||||
}
|
}
|
||||||
if (Array.isArray(el?.columns) && el.columns.length) {
|
if (Array.isArray(el?.columns) && el.columns.length) {
|
||||||
const headerRow = Array.isArray(el.columns[0]) ? el.columns[0] : [];
|
const headerRow = Array.isArray(el.columns[0]) ? el.columns[0] : [];
|
||||||
if (headerRow.length) {
|
if (headerRow.length) {
|
||||||
return compactColumnsForEditor(headerRow);
|
return mapColumnsKeepSequence(headerRow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
@@ -513,6 +529,87 @@ function(t,e,printData){
|
|||||||
return [...elements].sort((a, b) => score(b) - score(a))[0];
|
return [...elements].sort((a, b) => score(b) - score(a))[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 目标识别层:统一提列并打分(extractColumnsFromElement 同时兼容 options.columns/columns[0])
|
||||||
|
function pickBestColumnsFromTemplate(templateObj: any, dataKeySet: Set<string>) {
|
||||||
|
if (!templateObj || typeof templateObj !== 'object') return [];
|
||||||
|
const candidates: Array<{ node: any; cols: any[] }> = [];
|
||||||
|
const visited = new Set<any>();
|
||||||
|
const walk = (node: any) => {
|
||||||
|
if (!node || typeof node !== 'object' || visited.has(node)) return;
|
||||||
|
visited.add(node);
|
||||||
|
const cols = extractColumnsFromElement(node);
|
||||||
|
if (Array.isArray(cols) && cols.length > 0) {
|
||||||
|
candidates.push({ node, cols });
|
||||||
|
}
|
||||||
|
Object.keys(node).forEach((k) => {
|
||||||
|
const v = node[k];
|
||||||
|
if (Array.isArray(v)) v.forEach((it) => walk(it));
|
||||||
|
else if (v && typeof v === 'object') walk(v);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
walk(templateObj);
|
||||||
|
if (candidates.length === 0) return [];
|
||||||
|
|
||||||
|
const score = (item: { node: any; cols: any[] }) => {
|
||||||
|
let hit = 0;
|
||||||
|
let valid = 0;
|
||||||
|
item.cols.forEach((c: any) => {
|
||||||
|
const f = String(c?.field || '').trim();
|
||||||
|
if (!f) return;
|
||||||
|
valid++;
|
||||||
|
if (dataKeySet.has(f)) hit++;
|
||||||
|
});
|
||||||
|
const tidBonus = item?.node?.tid === 'qhmesModule.tableSimple' ? 50 : 0;
|
||||||
|
const tableFieldBonus = item?.node?.options?.field === 'table' ? 20 : 0;
|
||||||
|
return hit * 100 + valid * 20 + item.cols.length + tidBonus + tableFieldBonus;
|
||||||
|
};
|
||||||
|
|
||||||
|
const meaningful = candidates.filter((it) => it.cols.some((c: any) => !!String(c?.field || '').trim()));
|
||||||
|
const pool = meaningful.length > 0 ? meaningful : candidates;
|
||||||
|
const best = [...pool].sort((a, b) => score(b) - score(a))[0];
|
||||||
|
return compactColumnsForEditor(best?.cols || []).filter((c: any) => !!String(c?.field || '').trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickBestTableElementByDataKeys(templateObj: any, dataKeySet: Set<string>) {
|
||||||
|
if (!templateObj || typeof templateObj !== 'object') return null;
|
||||||
|
const candidates: Array<{ node: any; cols: any[] }> = [];
|
||||||
|
const visited = new Set<any>();
|
||||||
|
const walk = (node: any) => {
|
||||||
|
if (!node || typeof node !== 'object' || visited.has(node)) return;
|
||||||
|
visited.add(node);
|
||||||
|
const cols = extractColumnsFromElement(node);
|
||||||
|
if (Array.isArray(cols) && cols.length > 0) {
|
||||||
|
candidates.push({ node, cols });
|
||||||
|
}
|
||||||
|
Object.keys(node).forEach((k) => {
|
||||||
|
const v = node[k];
|
||||||
|
if (Array.isArray(v)) v.forEach((it) => walk(it));
|
||||||
|
else if (v && typeof v === 'object') walk(v);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
walk(templateObj);
|
||||||
|
if (candidates.length === 0) return null;
|
||||||
|
|
||||||
|
const score = (item: { node: any; cols: any[] }) => {
|
||||||
|
let hit = 0;
|
||||||
|
let valid = 0;
|
||||||
|
item.cols.forEach((c: any) => {
|
||||||
|
const f = String(c?.field || '').trim();
|
||||||
|
if (!f) return;
|
||||||
|
valid++;
|
||||||
|
if (dataKeySet.has(f)) hit++;
|
||||||
|
});
|
||||||
|
const tidBonus = item?.node?.tid === 'qhmesModule.tableSimple' ? 50 : 0;
|
||||||
|
const tableFieldBonus = item?.node?.options?.field === 'table' ? 20 : 0;
|
||||||
|
return hit * 100 + valid * 20 + item.cols.length + tidBonus + tableFieldBonus;
|
||||||
|
};
|
||||||
|
|
||||||
|
const meaningful = candidates.filter((it) => it.cols.some((c: any) => !!String(c?.field || '').trim()));
|
||||||
|
const pool = meaningful.length > 0 ? meaningful : candidates;
|
||||||
|
const best = [...pool].sort((a, b) => score(b) - score(a))[0];
|
||||||
|
return best?.node || null;
|
||||||
|
}
|
||||||
|
|
||||||
function patchTableSimpleElements(templateObj: any): number {
|
function patchTableSimpleElements(templateObj: any): number {
|
||||||
if (!templateObj) return 0;
|
if (!templateObj) return 0;
|
||||||
|
|
||||||
@@ -677,6 +774,7 @@ function(t,e,printData){
|
|||||||
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] : [];
|
||||||
const readCanvasTableStyle = () => {
|
const readCanvasTableStyle = () => {
|
||||||
@@ -837,6 +935,29 @@ function(t,e,printData){
|
|||||||
return previewTemplate;
|
return previewTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function logTableColumnsOnBoot() {
|
||||||
|
if (!hiprintTemplate) return;
|
||||||
|
const selectedEl = findCurrentTableElementFromCanvas();
|
||||||
|
const allEls = findAllTableElementsFromCanvas();
|
||||||
|
const bestEl = selectedEl || pickBestHeaderSource(allEls);
|
||||||
|
const cols = bestEl ? extractColumnsFromElement(bestEl) : [];
|
||||||
|
const normalized = normalizeColumns(cols).map((c) => ({
|
||||||
|
title: c.title,
|
||||||
|
order: c.order,
|
||||||
|
field: c.field,
|
||||||
|
width: c.width,
|
||||||
|
align: c.align,
|
||||||
|
headerBg: (c as any).headerBg || '',
|
||||||
|
}));
|
||||||
|
console.log('[PrintDesigner] 进入页面-强逻辑表格明细', {
|
||||||
|
source: selectedEl ? 'selected' : bestEl ? 'bestMatched' : 'none',
|
||||||
|
tid: bestEl?.tid || '',
|
||||||
|
type: bestEl?.type || '',
|
||||||
|
count: normalized.length,
|
||||||
|
columns: normalized,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function applyTableColumnsConfig(silent = false) {
|
function applyTableColumnsConfig(silent = false) {
|
||||||
let cols: SimpleColumn[] = [];
|
let cols: SimpleColumn[] = [];
|
||||||
try {
|
try {
|
||||||
@@ -867,6 +988,7 @@ function(t,e,printData){
|
|||||||
field: c.field,
|
field: c.field,
|
||||||
width: c.width,
|
width: c.width,
|
||||||
align: c.align,
|
align: c.align,
|
||||||
|
headerBg: (c as any).headerBg || '',
|
||||||
}));
|
}));
|
||||||
type.options.groupFields = groupEnabled.value ? [...groupFields.value] : [];
|
type.options.groupFields = groupEnabled.value ? [...groupFields.value] : [];
|
||||||
return type;
|
return type;
|
||||||
@@ -882,14 +1004,9 @@ function(t,e,printData){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildColumnsFromSampleData() {
|
async function buildColumnsFromSampleData(opts?: { silent?: boolean }) {
|
||||||
const data = parsePrintData();
|
const silent = !!opts?.silent;
|
||||||
const firstRow = Array.isArray(data?.table) && data.table.length > 0 ? data.table[0] : null;
|
// 与进入页面后的强逻辑保持一致:优先读画布组件本身的列定义,不再做字段推导和猜测
|
||||||
if (!firstRow || Object.prototype.toString.call(firstRow) !== '[object Object]') {
|
|
||||||
createMessage.warning('预览数据中未找到 table[0] 对象,无法推导');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 先强制提交右侧属性面板输入框,避免“已编辑但未提交”导致仍读到旧值
|
|
||||||
const commitPropertyPanelEdits = async () => {
|
const commitPropertyPanelEdits = async () => {
|
||||||
try {
|
try {
|
||||||
const container = document.querySelector('#PrintElementOptionSetting');
|
const container = document.querySelector('#PrintElementOptionSetting');
|
||||||
@@ -915,207 +1032,129 @@ function(t,e,printData){
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
await commitPropertyPanelEdits();
|
await commitPropertyPanelEdits();
|
||||||
|
const templateObj = parseTemplateObjectFromInstance();
|
||||||
// title 优先取画布里“最像人工表头”的明细组件
|
const firstRow = parsePrintData()?.table?.[0] || {};
|
||||||
|
const dataKeySet = new Set(Object.keys(firstRow || {}));
|
||||||
const selectedEl = findCurrentTableElementFromCanvas();
|
const selectedEl = findCurrentTableElementFromCanvas();
|
||||||
const allEls = findAllTableElementsFromCanvas();
|
const allEls = findAllTableElementsFromCanvas();
|
||||||
const bestEl = selectedEl || pickBestHeaderSource(allEls);
|
const bestDataEl = pickBestTableElementByDataKeys(templateObj, dataKeySet);
|
||||||
const existingCols = bestEl ? extractColumnsFromElement(bestEl) : [];
|
const bestEl = bestDataEl || selectedEl || pickBestHeaderSource(allEls);
|
||||||
const titleMap = new Map<string, string>();
|
let cols = bestEl ? extractColumnsFromElement(bestEl) : [];
|
||||||
existingCols.forEach((c: any) => {
|
if ((!Array.isArray(cols) || cols.length === 0) && templateObj) {
|
||||||
if (c?.field && c?.title) titleMap.set(c.field, c.title);
|
cols = pickBestColumnsFromTemplate(templateObj, dataKeySet);
|
||||||
});
|
|
||||||
|
|
||||||
// 一级优先:从右侧属性面板读取“标题”输入框(用户刚编辑但尚未回写时也能取到)
|
|
||||||
const readTitleFromPropertyPanel = () => {
|
|
||||||
try {
|
|
||||||
const container = document.querySelector('#PrintElementOptionSetting');
|
|
||||||
if (!container) return [] as string[];
|
|
||||||
const isColorLike = (v: string) => {
|
|
||||||
const s = (v || '').trim().toLowerCase();
|
|
||||||
return (
|
|
||||||
/^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(s) ||
|
|
||||||
/^rgba?\(/.test(s) ||
|
|
||||||
/^hsla?\(/.test(s) ||
|
|
||||||
/^(transparent|inherit|initial|unset|currentcolor)$/.test(s)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const rows = Array.from(container.querySelectorAll('.hiprint-option-item'));
|
|
||||||
const titles: string[] = [];
|
|
||||||
rows.forEach((row) => {
|
|
||||||
const labelEl = row.querySelector('.hiprint-option-item-label');
|
|
||||||
const label = (labelEl?.textContent || '').trim();
|
|
||||||
if (!label || !/(标题|title)/i.test(label)) return;
|
|
||||||
const inputEl =
|
|
||||||
(row.querySelector('input.auto-submit') as HTMLInputElement | null) ||
|
|
||||||
(row.querySelector('input') as HTMLInputElement | null) ||
|
|
||||||
(row.querySelector('textarea.auto-submit') as HTMLTextAreaElement | null) ||
|
|
||||||
(row.querySelector('textarea') as HTMLTextAreaElement | null);
|
|
||||||
const v = (inputEl?.value || '').trim();
|
|
||||||
if (v && !isColorLike(v)) titles.push(v);
|
|
||||||
});
|
|
||||||
// 兜底:如果上面没抓到,直接从属性面板抓取可见文本输入值(按顺序)
|
|
||||||
if (titles.length === 0) {
|
|
||||||
const allInputs = Array.from(
|
|
||||||
container.querySelectorAll('input[type="text"], input:not([type]), textarea')
|
|
||||||
) as Array<HTMLInputElement | HTMLTextAreaElement>;
|
|
||||||
allInputs.forEach((el) => {
|
|
||||||
const v = (el.value || '').trim();
|
|
||||||
if (!v) return;
|
|
||||||
// 过滤明显不是标题的值:纯数字、字段变量名样式
|
|
||||||
if (/^\d+(\.\d+)?$/.test(v)) return;
|
|
||||||
if (/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(v)) return;
|
|
||||||
if (isColorLike(v)) return;
|
|
||||||
titles.push(v);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return titles;
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(e);
|
|
||||||
return [] as string[];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const panelTitles = readTitleFromPropertyPanel();
|
|
||||||
if (panelTitles.length > 0) {
|
|
||||||
// 优先按当前组件列顺序映射
|
|
||||||
if (existingCols.length > 0 && panelTitles.length >= existingCols.length) {
|
|
||||||
existingCols.forEach((c: any, idx: number) => {
|
|
||||||
const t = panelTitles[idx];
|
|
||||||
if (c?.field && t) titleMap.set(c.field, t);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 组件列拿不到时,按预览数据字段顺序映射
|
|
||||||
Object.keys(firstRow).forEach((k, idx) => {
|
|
||||||
const t = panelTitles[idx];
|
|
||||||
if (t) titleMap.set(k, t);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// 二级兜底:使用当前文本框已有 title(用户手动维护过时)
|
if (Array.isArray(cols) && cols.length > 0) {
|
||||||
|
const readHeaderColorByField = () => {
|
||||||
|
try {
|
||||||
|
const tables = Array.from(document.querySelectorAll('#hiprint-printTemplate table')) as HTMLTableElement[];
|
||||||
|
if (!tables.length) return new Map<string, string>();
|
||||||
|
const isValidColor = (c: string) => !!c && c !== 'rgba(0, 0, 0, 0)' && c !== 'transparent';
|
||||||
|
const targetFields = new Set(cols.map((c: any) => String(c?.field || '').trim()).filter((f: string) => !!f));
|
||||||
|
const scoreTable = (tb: HTMLTableElement) => {
|
||||||
|
const cells = Array.from(tb.querySelectorAll('thead [colnum-id]')) as HTMLElement[];
|
||||||
|
const ids = cells.map((it) => String(it.getAttribute('colnum-id') || '').trim()).filter((v) => !!v);
|
||||||
|
const overlap = ids.filter((id) => targetFields.has(id)).length;
|
||||||
|
return overlap * 100 + ids.length * 10;
|
||||||
|
};
|
||||||
|
const tb = [...tables].sort((a, b) => scoreTable(b) - scoreTable(a))[0];
|
||||||
|
const thead = tb.querySelector('thead') as HTMLElement | null;
|
||||||
|
const trHead = tb.querySelector('thead tr') as HTMLElement | null;
|
||||||
|
const theadInlineBg = thead ? (thead.style.background || thead.style.backgroundColor || '') : '';
|
||||||
|
const theadBg = thead ? window.getComputedStyle(thead).backgroundColor || '' : '';
|
||||||
|
const trHeadBg = trHead ? window.getComputedStyle(trHead).backgroundColor || '' : '';
|
||||||
|
const map = new Map<string, string>();
|
||||||
|
const cells = Array.from(tb.querySelectorAll('thead [colnum-id]')) as HTMLElement[];
|
||||||
|
cells.forEach((cell) => {
|
||||||
|
const field = String(cell.getAttribute('colnum-id') || '').trim();
|
||||||
|
if (!field) return;
|
||||||
|
const inlineBg = (cell.style.background || cell.style.backgroundColor || '').trim();
|
||||||
|
const c = window.getComputedStyle(cell).backgroundColor || '';
|
||||||
|
const bg = isValidColor(inlineBg)
|
||||||
|
? inlineBg
|
||||||
|
: isValidColor(c)
|
||||||
|
? c
|
||||||
|
: isValidColor(theadInlineBg)
|
||||||
|
? theadInlineBg
|
||||||
|
: isValidColor(theadBg)
|
||||||
|
? theadBg
|
||||||
|
: isValidColor(trHeadBg)
|
||||||
|
? trHeadBg
|
||||||
|
: '';
|
||||||
|
if (bg) map.set(field, bg);
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e);
|
||||||
|
return new Map<string, string>();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const readUniformHeaderColor = () => {
|
||||||
|
try {
|
||||||
|
const tables = Array.from(document.querySelectorAll('#hiprint-printTemplate table')) as HTMLTableElement[];
|
||||||
|
if (!tables.length) return '';
|
||||||
|
const isValidColor = (c: string) => !!c && c !== 'rgba(0, 0, 0, 0)' && c !== 'transparent';
|
||||||
|
const tb = tables[0];
|
||||||
|
const thead = tb.querySelector('thead') as HTMLElement | null;
|
||||||
|
const trHead = tb.querySelector('thead tr') as HTMLElement | null;
|
||||||
|
const theadInlineBg = thead ? (thead.style.background || thead.style.backgroundColor || '') : '';
|
||||||
|
const theadBg = thead ? window.getComputedStyle(thead).backgroundColor || '' : '';
|
||||||
|
const trHeadBg = trHead ? window.getComputedStyle(trHead).backgroundColor || '' : '';
|
||||||
|
if (isValidColor(theadInlineBg)) return theadInlineBg;
|
||||||
|
if (isValidColor(theadBg)) return theadBg;
|
||||||
|
if (isValidColor(trHeadBg)) return trHeadBg;
|
||||||
|
return '';
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const headerColorMap = readHeaderColorByField();
|
||||||
|
const uniformHeaderColor = headerColorMap.size === 0 ? readUniformHeaderColor() : '';
|
||||||
|
const normalized = normalizeColumns(cols).map((c) => ({
|
||||||
|
title: c.title,
|
||||||
|
order: c.order,
|
||||||
|
field: c.field,
|
||||||
|
width: c.width,
|
||||||
|
align: c.align,
|
||||||
|
headerBg: c.headerBg || headerColorMap.get(String(c.field || '').trim()) || uniformHeaderColor || '',
|
||||||
|
}));
|
||||||
|
const strongHeaders = normalized.map((c) => String(c?.title || '').trim()).filter((t) => !!t);
|
||||||
|
console.log('[PrintDesigner] 数据命中明细列(强匹配)', normalized);
|
||||||
|
console.log('[PrintDesigner] 数据命中明细表头(强匹配)', strongHeaders);
|
||||||
|
tableColumnsJson.value = JSON.stringify(normalized, null, 2);
|
||||||
|
lastSyncedColumnsKey.value = JSON.stringify(normalized);
|
||||||
|
if (!silent) {
|
||||||
|
createMessage.success('已按画布表格配置同步列 JSON');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容场景:部分表格(如仅 content 的 HTML 表)无法反解析列,回退到当前已保存 JSON
|
||||||
try {
|
try {
|
||||||
const currentCols = JSON.parse(tableColumnsJson.value || '[]');
|
const cached = JSON.parse(tableColumnsJson.value || '[]');
|
||||||
if (Array.isArray(currentCols)) {
|
if (Array.isArray(cached) && cached.length > 0) {
|
||||||
currentCols.forEach((c: any) => {
|
const normalizedCached = normalizeColumns(cached).map((c) => ({
|
||||||
if (c?.field && c?.title && !titleMap.has(c.field)) {
|
title: c.title,
|
||||||
titleMap.set(c.field, c.title);
|
order: c.order,
|
||||||
}
|
field: c.field,
|
||||||
});
|
width: c.width,
|
||||||
|
align: c.align,
|
||||||
|
headerBg: c.headerBg || '',
|
||||||
|
}));
|
||||||
|
tableColumnsJson.value = JSON.stringify(normalizedCached, null, 2);
|
||||||
|
lastSyncedColumnsKey.value = JSON.stringify(normalizedCached);
|
||||||
|
if (!silent) {
|
||||||
|
createMessage.success('已按当前列 JSON 同步');
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e);
|
console.warn(e);
|
||||||
}
|
}
|
||||||
// 三级兜底:从画布可见表头 DOM 抓取标题(解决 title 一直是英文变量名)
|
|
||||||
const readHeaderTitlesFromCanvas = () => {
|
|
||||||
try {
|
|
||||||
const tables = Array.from(document.querySelectorAll('#hiprint-printTemplate table'));
|
|
||||||
let best: string[] = [];
|
|
||||||
tables.forEach((tb) => {
|
|
||||||
const ths = Array.from(tb.querySelectorAll('thead th'));
|
|
||||||
const titles = ths
|
|
||||||
.map((th) => (th.textContent || '').trim())
|
|
||||||
.filter((t) => !!t);
|
|
||||||
if (titles.length > best.length) best = titles;
|
|
||||||
});
|
|
||||||
return best;
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(e);
|
|
||||||
return [] as string[];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 字段顺序优先使用“画布可见表头顺序”映射,其次用画布明细列顺序,最后回退 JSON 键顺序
|
if (!silent) {
|
||||||
const canvasFieldOrder =
|
createMessage.warning('未识别到可读取列配置的表格组件,且当前列 JSON 为空');
|
||||||
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 t = titleMap.get(k);
|
|
||||||
return !t || t === k;
|
|
||||||
});
|
|
||||||
if (allEnglishLike) {
|
|
||||||
const domTitles = readHeaderTitlesFromCanvas();
|
|
||||||
if (domTitles.length > 0) {
|
|
||||||
// 优先按 existingCols 字段顺序映射(更接近画布列顺序)
|
|
||||||
if (existingCols.length > 0 && domTitles.length === existingCols.length) {
|
|
||||||
existingCols.forEach((c: any, idx: number) => {
|
|
||||||
if (c?.field && domTitles[idx]) {
|
|
||||||
titleMap.set(c.field, domTitles[idx]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (domTitles.length === keys.length) {
|
|
||||||
// 次选:按预览数据字段顺序映射
|
|
||||||
keys.forEach((k, idx) => {
|
|
||||||
if (domTitles[idx]) titleMap.set(k, domTitles[idx]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 最终兜底:常见字段中文名映射(确保按钮点击后可见变化)
|
|
||||||
const zhFallback: Record<string, string> = {
|
|
||||||
fbillno: '单号',
|
|
||||||
billno: '单号',
|
|
||||||
orderno: '订单号',
|
|
||||||
order_no: '订单号',
|
|
||||||
name: '物料',
|
|
||||||
material: '物料',
|
|
||||||
qty: '数量',
|
|
||||||
quantity: '数量',
|
|
||||||
amount: '金额',
|
|
||||||
unit: '单位',
|
|
||||||
price: '单价',
|
|
||||||
total: '合计',
|
|
||||||
};
|
|
||||||
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] 推导列配置');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTemplatePayload(str: string | undefined) {
|
function parseTemplatePayload(str: string | undefined) {
|
||||||
@@ -1139,6 +1178,59 @@ function(t,e,printData){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function migrateSingleHeaderTableElements(templateObj: any) {
|
||||||
|
if (!templateObj || typeof templateObj !== 'object') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const visited = new Set<any>();
|
||||||
|
const toTableColumns = (cols: any[]) => {
|
||||||
|
const normalized = normalizeColumns(Array.isArray(cols) ? cols : []);
|
||||||
|
if (normalized.length === 0) {
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
{ title: '单号', field: 'fbillno', width: 55, align: 'left', colspan: 1, rowspan: 1 },
|
||||||
|
{ title: '物料', field: 'name', width: 75, align: 'left', colspan: 1, rowspan: 1 },
|
||||||
|
{ title: '数量', field: 'qty', width: 35, align: 'right', colspan: 1, rowspan: 1 },
|
||||||
|
{ title: '金额', field: 'amount', width: 35, align: 'right', colspan: 1, rowspan: 1 },
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
normalized.map((c) => ({
|
||||||
|
title: c.title,
|
||||||
|
field: c.field,
|
||||||
|
width: c.width,
|
||||||
|
align: c.align,
|
||||||
|
colspan: 1,
|
||||||
|
rowspan: 1,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
const walk = (node: any) => {
|
||||||
|
if (!node || typeof node !== 'object' || visited.has(node)) return;
|
||||||
|
visited.add(node);
|
||||||
|
if (node?.tid === 'qhmesModule.tableSingleHeader' && node?.type !== 'table') {
|
||||||
|
node.type = 'table';
|
||||||
|
node.columns = toTableColumns(node?.options?.columns);
|
||||||
|
if (node.options && typeof node.options === 'object') {
|
||||||
|
delete node.options.formatter;
|
||||||
|
delete node.options.columns;
|
||||||
|
delete node.options.groupFields;
|
||||||
|
delete node.options.__qhmesManaged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Object.keys(node).forEach((k) => {
|
||||||
|
const v = node[k];
|
||||||
|
if (Array.isArray(v)) {
|
||||||
|
v.forEach((item) => walk(item));
|
||||||
|
} else if (v && typeof v === 'object') {
|
||||||
|
walk(v);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
walk(templateObj);
|
||||||
|
}
|
||||||
|
|
||||||
async function bootDesigner() {
|
async function bootDesigner() {
|
||||||
const id = route.query.id as string;
|
const id = route.query.id as string;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
@@ -1166,6 +1258,7 @@ function(t,e,printData){
|
|||||||
|
|
||||||
const payload = parseTemplatePayload(record.templateJson);
|
const payload = parseTemplatePayload(record.templateJson);
|
||||||
const template = payload.template;
|
const template = payload.template;
|
||||||
|
migrateSingleHeaderTableElements(template);
|
||||||
if (payload.ext) {
|
if (payload.ext) {
|
||||||
if (typeof payload.ext.printDataJson === 'string' && payload.ext.printDataJson.trim()) {
|
if (typeof payload.ext.printDataJson === 'string' && payload.ext.printDataJson.trim()) {
|
||||||
printDataJson.value = payload.ext.printDataJson;
|
printDataJson.value = payload.ext.printDataJson;
|
||||||
@@ -1187,6 +1280,13 @@ function(t,e,printData){
|
|||||||
hiprintTemplate.design('#hiprint-printTemplate');
|
hiprintTemplate.design('#hiprint-printTemplate');
|
||||||
applyFieldsToTemplate();
|
applyFieldsToTemplate();
|
||||||
applyTableColumnsConfig(true);
|
applyTableColumnsConfig(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
logTableColumnsOnBoot();
|
||||||
|
}, 120);
|
||||||
|
// 进入页面后自动执行一次与按钮一致的强匹配同步,确保顺序/颜色来自真实画布
|
||||||
|
setTimeout(() => {
|
||||||
|
buildColumnsFromSampleData({ silent: true });
|
||||||
|
}, 260);
|
||||||
// 实时从画布反向同步列配置(不打断手输)
|
// 实时从画布反向同步列配置(不打断手输)
|
||||||
if (columnsSyncTimer) clearInterval(columnsSyncTimer);
|
if (columnsSyncTimer) clearInterval(columnsSyncTimer);
|
||||||
columnsSyncTimer = setInterval(() => {
|
columnsSyncTimer = setInterval(() => {
|
||||||
@@ -1262,6 +1362,7 @@ function(t,e,printData){
|
|||||||
field: c.field,
|
field: c.field,
|
||||||
width: c.width,
|
width: c.width,
|
||||||
align: c.align,
|
align: c.align,
|
||||||
|
headerBg: (c as any).headerBg || '',
|
||||||
}));
|
}));
|
||||||
data.__qhmesGroupFields = groupEnabled.value ? [...groupFields.value] : [];
|
data.__qhmesGroupFields = groupEnabled.value ? [...groupFields.value] : [];
|
||||||
const previewTemplate = buildPreviewTemplateWithGrouping();
|
const previewTemplate = buildPreviewTemplateWithGrouping();
|
||||||
|
|||||||
@@ -180,6 +180,26 @@ function(t,e,printData){
|
|||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
tid: `${key}.tableSingleHeader`,
|
||||||
|
title: '单行表头表格',
|
||||||
|
type: 'table',
|
||||||
|
options: {
|
||||||
|
title: '单行表头表格',
|
||||||
|
field: 'table',
|
||||||
|
testData: '',
|
||||||
|
width: 180,
|
||||||
|
height: 60,
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
[
|
||||||
|
{ title: '单号', field: 'fbillno', width: 55, align: 'left', colspan: 1, rowspan: 1 },
|
||||||
|
{ title: '物料', field: 'name', width: 75, align: 'left', colspan: 1, rowspan: 1 },
|
||||||
|
{ title: '数量', field: 'qty', width: 35, align: 'right', colspan: 1, rowspan: 1 },
|
||||||
|
{ title: '金额', field: 'amount', width: 35, align: 'right', colspan: 1, rowspan: 1 },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// 分组(左侧面板展示更友好)
|
// 分组(左侧面板展示更友好)
|
||||||
|
|||||||
Reference in New Issue
Block a user