diff --git a/jeecgboot-vue3/src/views/print/template/PrintDesigner.vue b/jeecgboot-vue3/src/views/print/template/PrintDesigner.vue index a3e91cf..7a296fc 100644 --- a/jeecgboot-vue3/src/views/print/template/PrintDesigner.vue +++ b/jeecgboot-vue3/src/views/print/template/PrintDesigner.vue @@ -62,6 +62,8 @@ v-model:value="tableColumnsJson" :rows="6" placeholder='例如:[{"title":"物料","field":"name","width":90},{"title":"数量","field":"qty","width":45}]' + @focus="onColumnsEditorFocus" + @blur="onColumnsEditorBlur" /> 从预览数据推导列 @@ -127,6 +129,9 @@ let hiprintTemplate: any = null; let hiprintInited = false; let fieldsTimer: any = null; + let columnsSyncTimer: any = null; + const columnsEditorFocused = ref(false); + const lastSyncedColumnsKey = ref(''); function ensureJqueryGlobal() { (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() { // 与 qhmesProvider.ts 保持一致:多级分组按 groupFields 顺序计算 rowspan return ` @@ -362,6 +384,112 @@ function(t,e,printData){ 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(); + 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('(); + 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(' { + 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 { 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(); + 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(' 0) { + groupEnabled.value = true; + groupFields.value = gfs; + } + } + } + function buildPreviewTemplateWithGrouping() { const templateObj = parseTemplateObjectFromInstance(); if (!templateObj) return null; @@ -579,15 +773,188 @@ function(t,e,printData){ } } - function buildColumnsFromSampleData() { + async function buildColumnsFromSampleData() { const data = parsePrintData(); 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 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; + 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(); + 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; + 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 = { + 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, width: 60, align: typeof firstRow[k] === 'number' ? 'right' : 'left', @@ -694,6 +1061,11 @@ function(t,e,printData){ hiprintTemplate.design('#hiprint-printTemplate'); applyFieldsToTemplate(); applyTableColumnsConfig(true); + // 实时从画布反向同步列配置(不打断手输) + if (columnsSyncTimer) clearInterval(columnsSyncTimer); + columnsSyncTimer = setInterval(() => { + syncColumnsConfigFromCanvas(); + }, 800); } function goBack() { @@ -801,6 +1173,10 @@ function(t,e,printData){ clearTimeout(fieldsTimer); fieldsTimer = null; } + if (columnsSyncTimer) { + clearInterval(columnsSyncTimer); + columnsSyncTimer = null; + } destroyDesigner(); });