|
|
const FONT_SIZE_MAP = {
|
|
|
1: '14px',
|
|
|
2: '18px',
|
|
|
3: '28px',
|
|
|
}
|
|
|
|
|
|
const escapeHtml = (value) => {
|
|
|
return String(value ?? '')
|
|
|
.replace(/&/g, '&')
|
|
|
.replace(/</g, '<')
|
|
|
.replace(/>/g, '>')
|
|
|
.replace(/"/g, '"')
|
|
|
.replace(/'/g, ''')
|
|
|
}
|
|
|
|
|
|
const replacePlaceholders = (content, data) => {
|
|
|
return String(content || '').replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
|
|
const val = data[key]
|
|
|
return val !== undefined && val !== null ? String(val) : ''
|
|
|
})
|
|
|
}
|
|
|
|
|
|
export const PLACEHOLDER_TEXT_CONTENTS = ['{{title}}', '{{ticketNo}}']
|
|
|
|
|
|
export const isPlaceholderTextSection = (section) => {
|
|
|
if (section?.type !== 'text') return false
|
|
|
const content = String(section.content || '').trim()
|
|
|
return PLACEHOLDER_TEXT_CONTENTS.includes(content)
|
|
|
}
|
|
|
|
|
|
const normalizeTextSection = (section) => {
|
|
|
const normalized = {
|
|
|
type: 'text',
|
|
|
content: String(section.content || ''),
|
|
|
fontSize: Number(section.fontSize) || 1,
|
|
|
align: section.align || 'left',
|
|
|
}
|
|
|
if (section.bold) {
|
|
|
normalized.bold = true
|
|
|
}
|
|
|
return normalized
|
|
|
}
|
|
|
|
|
|
const normalizeFieldSection = (section) => {
|
|
|
const normalized = {
|
|
|
type: 'field',
|
|
|
key: String(section.key || ''),
|
|
|
label: String(section.label || ''),
|
|
|
show: section.show !== false,
|
|
|
}
|
|
|
if (section.suffix) {
|
|
|
normalized.suffix = String(section.suffix)
|
|
|
}
|
|
|
return normalized
|
|
|
}
|
|
|
|
|
|
const normalizeSection = (section) => {
|
|
|
if (!section || !section.type) return null
|
|
|
if (section.type === 'line') {
|
|
|
return { type: 'line' }
|
|
|
}
|
|
|
if (section.type === 'text') {
|
|
|
return normalizeTextSection(section)
|
|
|
}
|
|
|
if (section.type === 'field') {
|
|
|
return normalizeFieldSection(section)
|
|
|
}
|
|
|
return null
|
|
|
}
|
|
|
|
|
|
export const normalizePrintTemplate = (template) => {
|
|
|
const sections = Array.isArray(template?.sections) ? template.sections : []
|
|
|
return {
|
|
|
cutPaper: template?.cutPaper !== false,
|
|
|
sections: sections.map(normalizeSection).filter(Boolean),
|
|
|
}
|
|
|
}
|
|
|
|
|
|
export const parsePrintTemplate = (raw) => {
|
|
|
console.log('[print-template] parsePrintTemplate 输入:', raw)
|
|
|
|
|
|
if (raw === undefined || raw === null) {
|
|
|
console.log('[print-template] 输入为空')
|
|
|
return normalizePrintTemplate({ sections: [], cutPaper: true })
|
|
|
}
|
|
|
|
|
|
if (typeof raw === 'string') {
|
|
|
const text = raw.trim()
|
|
|
if (!text) {
|
|
|
return normalizePrintTemplate({ sections: [], cutPaper: true })
|
|
|
}
|
|
|
try {
|
|
|
return parsePrintTemplate(JSON.parse(text))
|
|
|
} catch (error) {
|
|
|
console.log('[print-template] JSON 字符串解析失败:', error, text)
|
|
|
return normalizePrintTemplate({ sections: [], cutPaper: true })
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (Array.isArray(raw.sections)) {
|
|
|
const result = normalizePrintTemplate(raw)
|
|
|
console.log('[print-template] 直接解析 sections,数量:', result.sections.length)
|
|
|
return result
|
|
|
}
|
|
|
|
|
|
if (typeof raw.msg === 'string' && raw.msg.trim()) {
|
|
|
try {
|
|
|
const parsed = JSON.parse(raw.msg)
|
|
|
if (parsed && Array.isArray(parsed.sections)) {
|
|
|
const result = normalizePrintTemplate(parsed)
|
|
|
console.log('[print-template] 从 msg 解析,sections 数量:', result.sections.length)
|
|
|
return result
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.log('[print-template] msg JSON 解析失败:', error, raw.msg)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (raw.data !== undefined && raw.data !== null) {
|
|
|
console.log('[print-template] 尝试解析 data 字段:', raw.data)
|
|
|
return parsePrintTemplate(raw.data)
|
|
|
}
|
|
|
|
|
|
console.log('[print-template] 未能识别模板结构,可用字段:', Object.keys(raw))
|
|
|
return normalizePrintTemplate({ sections: [], cutPaper: true })
|
|
|
}
|
|
|
|
|
|
// 保存/获取统一格式:{ cutPaper, sections }
|
|
|
export const buildPrintTemplateSaveBody = (template) => {
|
|
|
return normalizePrintTemplate(template)
|
|
|
}
|
|
|
|
|
|
// 极小模板,用于排查 sections 过大是否导致签名/网关异常
|
|
|
export const getMinimalPrintTemplateForTest = () =>
|
|
|
normalizePrintTemplate({
|
|
|
cutPaper: true,
|
|
|
sections: [
|
|
|
{ type: 'text', content: '测试', fontSize: 1, align: 'center' },
|
|
|
],
|
|
|
})
|
|
|
|
|
|
export const PRINT_SAMPLE_DATA = {
|
|
|
title: '国家税务总局',
|
|
|
ticketNo: 'A001',
|
|
|
businessType: '综合业务',
|
|
|
waitingCount: 5,
|
|
|
windowNumber: '3号窗口',
|
|
|
}
|
|
|
|
|
|
export const getDefaultPrintTemplate = () =>
|
|
|
normalizePrintTemplate({
|
|
|
cutPaper: true,
|
|
|
sections: [
|
|
|
{ type: 'text', content: '{{title}}', fontSize: 2, bold: true, align: 'center' },
|
|
|
{ type: 'line' },
|
|
|
{ type: 'text', content: '{{ticketNo}}', fontSize: 3, bold: true, align: 'center' },
|
|
|
{ type: 'line' },
|
|
|
{ type: 'field', key: 'businessType', label: '业务', show: true },
|
|
|
{ type: 'field', key: 'waitingCount', label: '等候', show: true, suffix: ' 人' },
|
|
|
{ type: 'field', key: 'windowNumber', label: '地点', show: true },
|
|
|
{ type: 'line' },
|
|
|
{ type: 'text', content: '请留意广播和显示屏叫号', fontSize: 1, align: 'center' },
|
|
|
],
|
|
|
})
|
|
|
|
|
|
export const buildPrintPreviewData = (template, context = {}) => {
|
|
|
return {
|
|
|
title: context.title ?? '',
|
|
|
ticketNo: context.ticketNo ?? '——',
|
|
|
businessType: context.businessType ?? context.businessName ?? '-',
|
|
|
waitingCount: context.waitingCount ?? 0,
|
|
|
windowNumber: context.windowNumber ?? '-',
|
|
|
rankUserName: context.rankUserName ?? '',
|
|
|
...context,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
export const renderPrintTemplateHtml = (template, data = {}) => {
|
|
|
if (!template || !Array.isArray(template.sections)) {
|
|
|
return '<p style="text-align:center;color:#999;">暂无打印模板</p>'
|
|
|
}
|
|
|
|
|
|
const parts = [
|
|
|
'<div style="font-family:SimHei,monospace;padding:16px 12px;background:#fff;color:#111;">',
|
|
|
]
|
|
|
|
|
|
template.sections.forEach((section) => {
|
|
|
if (section?.show === false) return
|
|
|
|
|
|
if (section.type === 'line') {
|
|
|
parts.push('<div style="border-top:1px dashed #999;margin:10px 0;"></div>')
|
|
|
return
|
|
|
}
|
|
|
|
|
|
if (section.type === 'text') {
|
|
|
const text = replacePlaceholders(section.content, data)
|
|
|
const fontSize = FONT_SIZE_MAP[section.fontSize] || FONT_SIZE_MAP[1]
|
|
|
const fontWeight = section.bold ? 'bold' : 'normal'
|
|
|
const textAlign = section.align || 'left'
|
|
|
parts.push(
|
|
|
`<p style="margin:6px 0;font-size:${fontSize};font-weight:${fontWeight};text-align:${textAlign};line-height:1.4;">${escapeHtml(text)}</p>`
|
|
|
)
|
|
|
return
|
|
|
}
|
|
|
|
|
|
if (section.type === 'field') {
|
|
|
const value = data[section.key]
|
|
|
const display =
|
|
|
value !== undefined && value !== null && value !== '' ? String(value) : '-'
|
|
|
const suffix = section.suffix || ''
|
|
|
const label = section.label || ''
|
|
|
parts.push(
|
|
|
`<p style="margin:6px 0;font-size:14px;line-height:1.5;"><span>${escapeHtml(label)}:</span><span>${escapeHtml(display)}${escapeHtml(suffix)}</span></p>`
|
|
|
)
|
|
|
}
|
|
|
})
|
|
|
|
|
|
parts.push('</div>')
|
|
|
return parts.join('')
|
|
|
}
|