增强PrintDesigner组件,添加列编辑器聚焦和失焦事件处理,优化列同步逻辑,新增从画布提取列和分组字段的功能。
This commit is contained in:
@@ -62,6 +62,8 @@
|
|||||||
v-model:value="tableColumnsJson"
|
v-model:value="tableColumnsJson"
|
||||||
:rows="6"
|
:rows="6"
|
||||||
placeholder='例如:[{"title":"物料","field":"name","width":90},{"title":"数量","field":"qty","width":45}]'
|
placeholder='例如:[{"title":"物料","field":"name","width":90},{"title":"数量","field":"qty","width":45}]'
|
||||||
|
@focus="onColumnsEditorFocus"
|
||||||
|
@blur="onColumnsEditorBlur"
|
||||||
/>
|
/>
|
||||||
<a-space>
|
<a-space>
|
||||||
<a-button @click="buildColumnsFromSampleData">从预览数据推导列</a-button>
|
<a-button @click="buildColumnsFromSampleData">从预览数据推导列</a-button>
|
||||||
@@ -127,6 +129,9 @@
|
|||||||
let hiprintTemplate: any = null;
|
let hiprintTemplate: any = null;
|
||||||
let hiprintInited = false;
|
let hiprintInited = false;
|
||||||
let fieldsTimer: any = null;
|
let fieldsTimer: any = null;
|
||||||
|
let columnsSyncTimer: any = null;
|
||||||
|
const columnsEditorFocused = ref(false);
|
||||||
|
const lastSyncedColumnsKey = ref('');
|
||||||
|
|
||||||
function ensureJqueryGlobal() {
|
function ensureJqueryGlobal() {
|
||||||
(window as any).$ = $;
|
(window as any).$ = $;
|
||||||
@@ -266,6 +271,23 @@
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function compactColumnsForEditor(cols: any[]) {
|
||||||
|
return normalizeColumns(cols).map((c) => ({
|
||||||
|
title: c.title,
|
||||||
|
field: c.field,
|
||||||
|
width: c.width,
|
||||||
|
align: c.align,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onColumnsEditorFocus() {
|
||||||
|
columnsEditorFocused.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onColumnsEditorBlur() {
|
||||||
|
columnsEditorFocused.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
function getTableSimpleFormatter() {
|
function getTableSimpleFormatter() {
|
||||||
// 与 qhmesProvider.ts 保持一致:多级分组按 groupFields 顺序计算 rowspan
|
// 与 qhmesProvider.ts 保持一致:多级分组按 groupFields 顺序计算 rowspan
|
||||||
return `
|
return `
|
||||||
@@ -362,6 +384,112 @@ function(t,e,printData){
|
|||||||
return json || null;
|
return json || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractColumnsFromElement(el: any) {
|
||||||
|
if (Array.isArray(el?.options?.columns) && el.options.columns.length) {
|
||||||
|
return compactColumnsForEditor(el.options.columns);
|
||||||
|
}
|
||||||
|
if (Array.isArray(el?.columns) && el.columns.length) {
|
||||||
|
const headerRow = Array.isArray(el.columns[0]) ? el.columns[0] : [];
|
||||||
|
if (headerRow.length) {
|
||||||
|
return compactColumnsForEditor(headerRow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractGroupFieldsFromElement(el: any) {
|
||||||
|
if (Array.isArray(el?.options?.groupFields)) {
|
||||||
|
return [...el.options.groupFields];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function findCurrentTableElementFromCanvas() {
|
||||||
|
if (!hiprintTemplate) return null;
|
||||||
|
const templateObj = parseTemplateObjectFromInstance();
|
||||||
|
if (!templateObj) return null;
|
||||||
|
try {
|
||||||
|
const selected = hiprintTemplate.getSelectEls?.();
|
||||||
|
if (Array.isArray(selected) && selected.length > 0) {
|
||||||
|
const hit = selected.find((el: any) => {
|
||||||
|
const tid = el?.tid;
|
||||||
|
const type = el?.type;
|
||||||
|
return tid === 'qhmesModule.tableSimple' || tid === 'defaultModule.table' || type === 'table' || type === 'html';
|
||||||
|
});
|
||||||
|
if (hit) return hit;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e);
|
||||||
|
}
|
||||||
|
const visited = new Set<any>();
|
||||||
|
const walk = (node: any): any => {
|
||||||
|
if (!node || typeof node !== 'object' || visited.has(node)) return null;
|
||||||
|
visited.add(node);
|
||||||
|
const isTableLike =
|
||||||
|
node?.tid === 'qhmesModule.tableSimple' ||
|
||||||
|
node?.tid === 'defaultModule.table' ||
|
||||||
|
node?.type === 'table' ||
|
||||||
|
(node?.type === 'html' && typeof node?.options?.formatter === 'string' && node.options.formatter.includes('<table'));
|
||||||
|
if (isTableLike) return node;
|
||||||
|
for (const k of Object.keys(node)) {
|
||||||
|
const v = node[k];
|
||||||
|
if (Array.isArray(v)) {
|
||||||
|
for (const item of v) {
|
||||||
|
const found = walk(item);
|
||||||
|
if (found) return found;
|
||||||
|
}
|
||||||
|
} else if (v && typeof v === 'object') {
|
||||||
|
const found = walk(v);
|
||||||
|
if (found) return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
return walk(templateObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findAllTableElementsFromCanvas() {
|
||||||
|
if (!hiprintTemplate) return [];
|
||||||
|
const templateObj = parseTemplateObjectFromInstance();
|
||||||
|
if (!templateObj) return [];
|
||||||
|
const result: any[] = [];
|
||||||
|
const visited = new Set<any>();
|
||||||
|
const walk = (node: any) => {
|
||||||
|
if (!node || typeof node !== 'object' || visited.has(node)) return;
|
||||||
|
visited.add(node);
|
||||||
|
const isTableLike =
|
||||||
|
node?.tid === 'qhmesModule.tableSimple' ||
|
||||||
|
node?.tid === 'defaultModule.table' ||
|
||||||
|
node?.type === 'table' ||
|
||||||
|
(node?.type === 'html' && typeof node?.options?.formatter === 'string' && node.options.formatter.includes('<table'));
|
||||||
|
if (isTableLike) result.push(node);
|
||||||
|
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);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickBestHeaderSource(elements: any[]) {
|
||||||
|
if (!Array.isArray(elements) || elements.length === 0) return null;
|
||||||
|
const score = (el: any) => {
|
||||||
|
const cols = extractColumnsFromElement(el);
|
||||||
|
if (!cols.length) return -1;
|
||||||
|
let s = cols.length; // 列越多越优先
|
||||||
|
cols.forEach((c: any) => {
|
||||||
|
const t = String(c?.title || '');
|
||||||
|
const f = String(c?.field || '');
|
||||||
|
if (t && f && t !== f) s += 3; // 标题与字段不同,说明是人工表头
|
||||||
|
if (/[^\x00-\x7F]/.test(t)) s += 4; // 含中文再加权
|
||||||
|
});
|
||||||
|
return s;
|
||||||
|
};
|
||||||
|
return [...elements].sort((a, b) => score(b) - score(a))[0];
|
||||||
|
}
|
||||||
|
|
||||||
function patchTableSimpleElements(templateObj: any): number {
|
function patchTableSimpleElements(templateObj: any): number {
|
||||||
if (!templateObj) return 0;
|
if (!templateObj) return 0;
|
||||||
|
|
||||||
@@ -440,6 +568,72 @@ function(t,e,printData){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncColumnsConfigFromCanvas() {
|
||||||
|
if (!hiprintTemplate || columnsEditorFocused.value) return;
|
||||||
|
const templateObj = parseTemplateObjectFromInstance();
|
||||||
|
if (!templateObj) return;
|
||||||
|
|
||||||
|
// 优先取当前选中的元素
|
||||||
|
let targetEl: any = null;
|
||||||
|
try {
|
||||||
|
const selected = hiprintTemplate.getSelectEls?.();
|
||||||
|
if (Array.isArray(selected) && selected.length > 0) {
|
||||||
|
targetEl = selected.find((el: any) => {
|
||||||
|
const tid = el?.tid;
|
||||||
|
const type = el?.type;
|
||||||
|
return tid === 'qhmesModule.tableSimple' || tid === 'defaultModule.table' || type === 'table' || type === 'html';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 若无选中,则取模板中第一个可识别明细组件
|
||||||
|
if (!targetEl) {
|
||||||
|
const visited = new Set<any>();
|
||||||
|
const walk = (node: any): any => {
|
||||||
|
if (!node || typeof node !== 'object' || visited.has(node)) return null;
|
||||||
|
visited.add(node);
|
||||||
|
const isTableLike =
|
||||||
|
node?.tid === 'qhmesModule.tableSimple' ||
|
||||||
|
node?.tid === 'defaultModule.table' ||
|
||||||
|
node?.type === 'table' ||
|
||||||
|
(node?.type === 'html' && typeof node?.options?.formatter === 'string' && node.options.formatter.includes('<table'));
|
||||||
|
if (isTableLike) return node;
|
||||||
|
for (const k of Object.keys(node)) {
|
||||||
|
const v = node[k];
|
||||||
|
if (Array.isArray(v)) {
|
||||||
|
for (const item of v) {
|
||||||
|
const found = walk(item);
|
||||||
|
if (found) return found;
|
||||||
|
}
|
||||||
|
} else if (v && typeof v === 'object') {
|
||||||
|
const found = walk(v);
|
||||||
|
if (found) return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
targetEl = walk(templateObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetEl) return;
|
||||||
|
|
||||||
|
const cols = extractColumnsFromElement(targetEl);
|
||||||
|
if (cols.length === 0) return;
|
||||||
|
const nextJson = JSON.stringify(cols, null, 2);
|
||||||
|
const nextKey = JSON.stringify(cols);
|
||||||
|
if (nextKey !== lastSyncedColumnsKey.value) {
|
||||||
|
lastSyncedColumnsKey.value = nextKey;
|
||||||
|
tableColumnsJson.value = nextJson;
|
||||||
|
const gfs = extractGroupFieldsFromElement(targetEl);
|
||||||
|
if (gfs.length > 0) {
|
||||||
|
groupEnabled.value = true;
|
||||||
|
groupFields.value = gfs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function buildPreviewTemplateWithGrouping() {
|
function buildPreviewTemplateWithGrouping() {
|
||||||
const templateObj = parseTemplateObjectFromInstance();
|
const templateObj = parseTemplateObjectFromInstance();
|
||||||
if (!templateObj) return null;
|
if (!templateObj) return null;
|
||||||
@@ -579,15 +773,188 @@ function(t,e,printData){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildColumnsFromSampleData() {
|
async function buildColumnsFromSampleData() {
|
||||||
const data = parsePrintData();
|
const data = parsePrintData();
|
||||||
const firstRow = Array.isArray(data?.table) && data.table.length > 0 ? data.table[0] : null;
|
const firstRow = Array.isArray(data?.table) && data.table.length > 0 ? data.table[0] : null;
|
||||||
if (!firstRow || Object.prototype.toString.call(firstRow) !== '[object Object]') {
|
if (!firstRow || Object.prototype.toString.call(firstRow) !== '[object Object]') {
|
||||||
createMessage.warning('预览数据中未找到 table[0] 对象,无法推导');
|
createMessage.warning('预览数据中未找到 table[0] 对象,无法推导');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const cols = Object.keys(firstRow).map((k) => ({
|
// 先强制提交右侧属性面板输入框,避免“已编辑但未提交”导致仍读到旧值
|
||||||
title: k,
|
const commitPropertyPanelEdits = async () => {
|
||||||
|
try {
|
||||||
|
const container = document.querySelector('#PrintElementOptionSetting');
|
||||||
|
if (!container) return;
|
||||||
|
const els = Array.from(
|
||||||
|
container.querySelectorAll('input,textarea,select,.auto-submit')
|
||||||
|
) as Array<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | Element>;
|
||||||
|
els.forEach((el: any) => {
|
||||||
|
try {
|
||||||
|
if (typeof el.dispatchEvent === 'function') {
|
||||||
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
el.dispatchEvent(new FocusEvent('blur', { bubbles: true }));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 80));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await commitPropertyPanelEdits();
|
||||||
|
|
||||||
|
// title 优先取画布里“最像人工表头”的明细组件
|
||||||
|
const selectedEl = findCurrentTableElementFromCanvas();
|
||||||
|
const allEls = findAllTableElementsFromCanvas();
|
||||||
|
const bestEl = selectedEl || pickBestHeaderSource(allEls);
|
||||||
|
const existingCols = bestEl ? extractColumnsFromElement(bestEl) : [];
|
||||||
|
const titleMap = new Map<string, string>();
|
||||||
|
existingCols.forEach((c: any) => {
|
||||||
|
if (c?.field && c?.title) titleMap.set(c.field, c.title);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 一级优先:从右侧属性面板读取“标题”输入框(用户刚编辑但尚未回写时也能取到)
|
||||||
|
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(用户手动维护过时)
|
||||||
|
try {
|
||||||
|
const currentCols = JSON.parse(tableColumnsJson.value || '[]');
|
||||||
|
if (Array.isArray(currentCols)) {
|
||||||
|
currentCols.forEach((c: any) => {
|
||||||
|
if (c?.field && c?.title && !titleMap.has(c.field)) {
|
||||||
|
titleMap.set(c.field, c.title);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (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[];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const keys = Object.keys(firstRow);
|
||||||
|
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) => ({
|
||||||
|
title: titleMap.get(k) || zhFallback[k.toLowerCase()] || k,
|
||||||
field: k,
|
field: k,
|
||||||
width: 60,
|
width: 60,
|
||||||
align: typeof firstRow[k] === 'number' ? 'right' : 'left',
|
align: typeof firstRow[k] === 'number' ? 'right' : 'left',
|
||||||
@@ -694,6 +1061,11 @@ function(t,e,printData){
|
|||||||
hiprintTemplate.design('#hiprint-printTemplate');
|
hiprintTemplate.design('#hiprint-printTemplate');
|
||||||
applyFieldsToTemplate();
|
applyFieldsToTemplate();
|
||||||
applyTableColumnsConfig(true);
|
applyTableColumnsConfig(true);
|
||||||
|
// 实时从画布反向同步列配置(不打断手输)
|
||||||
|
if (columnsSyncTimer) clearInterval(columnsSyncTimer);
|
||||||
|
columnsSyncTimer = setInterval(() => {
|
||||||
|
syncColumnsConfigFromCanvas();
|
||||||
|
}, 800);
|
||||||
}
|
}
|
||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
@@ -801,6 +1173,10 @@ function(t,e,printData){
|
|||||||
clearTimeout(fieldsTimer);
|
clearTimeout(fieldsTimer);
|
||||||
fieldsTimer = null;
|
fieldsTimer = null;
|
||||||
}
|
}
|
||||||
|
if (columnsSyncTimer) {
|
||||||
|
clearInterval(columnsSyncTimer);
|
||||||
|
columnsSyncTimer = null;
|
||||||
|
}
|
||||||
destroyDesigner();
|
destroyDesigner();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user