/** * 与 Web 管理端 labelFormDatePreview 一致:templateProductDefaults 中日期/时间/时长存 JSON * `{"unit":"Days","value":"2"}`,预览与打印时按 BaseTime 解析为 config.text。 */ import type { SystemTemplateElementBase } from '../print/types/printer' const OFFSET_UNITS = new Set([ 'Minutes', 'Hours', 'Days', 'Weeks', 'Months (30 Day)', 'Years', ]) export function applyOffsetToDate (base: Date, amount: number, unit: string): Date { const d = new Date(base.getTime()) const u = String(unit ?? '').trim() switch (u) { case 'Minutes': d.setMinutes(d.getMinutes() + amount) break case 'Hours': d.setHours(d.getHours() + amount) break case 'Days': d.setDate(d.getDate() + amount) break case 'Weeks': d.setDate(d.getDate() + amount * 7) break case 'Months (30 Day)': d.setDate(d.getDate() + amount * 30) break case 'Years': d.setFullYear(d.getFullYear() + amount) break default: d.setDate(d.getDate() + amount) } return d } function formatDateByPreset (format: string, date: Date): string { const yyyy = String(date.getFullYear()) const yy = yyyy.slice(-2) const mm = String(date.getMonth() + 1).padStart(2, '0') const dd = String(date.getDate()).padStart(2, '0') const hh = String(date.getHours()).padStart(2, '0') const min = String(date.getMinutes()).padStart(2, '0') switch (format) { case 'DD/MM/YYYY': return `${dd}/${mm}/${yyyy}` case 'MM/DD/YYYY': return `${mm}/${dd}/${yyyy}` case 'DD/MM/YY': return `${dd}/${mm}/${yy}` case 'MM/DD/YY': return `${mm}/${dd}/${yy}` case 'MM/YY': return `${mm}/${yy}` case 'MM/DD': return `${mm}/${dd}` case 'MM': return mm case 'DD': return dd case 'YY': return yy case 'YYYY-MM-DD': return `${yyyy}-${mm}-${dd}` case 'YYYY-MM-DD HH:mm': return `${yyyy}-${mm}-${dd} ${hh}:${min}` case 'HH:mm': return `${hh}:${min}` default: return String(format || '') .replace(/YYYY/g, yyyy) .replace(/YY/g, yy) .replace(/MM/g, mm) .replace(/DD/g, dd) .replace(/HH/g, hh) .replace(/mm/g, min) } } /** 已算好的日期/时间展示串(非 format 预设名、非 JSON 偏移) */ export function isLikelyResolvedDateTimeLiteral (raw: string | null | undefined): boolean { const t = String(raw ?? '').trim() if (!t || t.startsWith('{')) return false if (OFFSET_UNITS.has(t)) return false if (/^\d{4}-\d{2}-\d{2}/.test(t)) return true if (/^\d{4}-\d{2}-\d{2}\s+\d{1,2}:\d{2}/.test(t)) return true if (/^\d{1,2}\/\d{1,2}\/\d{2,4}/.test(t)) return true if (/^\d{1,2}:\d{2}(:\d{2})?$/.test(t)) return true return false } function applyElementPrefix (cfg: Record, body: string): string { const prefix = String(cfg.prefix ?? cfg.Prefix ?? '') if (!prefix) return body if (body.startsWith(prefix)) return body return `${prefix}${body}` } function normalizeOffsetAmount (valueRaw: string): number | null { const t = String(valueRaw ?? '').trim() if (t === '') return 0 const n = Number(t) return Number.isFinite(n) ? n : null } function dateFormatPatternForElement ( el: SystemTemplateElementBase, cfg: Record, ): string { const type = String(el.type || '').toUpperCase() if (type === 'TIME') return 'HH:mm' const it = String(cfg.inputType ?? cfg.InputType ?? '').toLowerCase() const raw = (typeof cfg.format === 'string' && cfg.format.trim() ? cfg.format : typeof cfg.Format === 'string' && cfg.Format.trim() ? cfg.Format : it === 'datetime' ? 'YYYY-MM-DD HH:mm' : 'DD/MM/YYYY') || 'DD/MM/YYYY' if (isLikelyResolvedDateTimeLiteral(raw) || OFFSET_UNITS.has(raw)) { return it === 'datetime' ? 'YYYY-MM-DD HH:mm' : 'DD/MM/YYYY' } return raw } function readOffsetFromElementConfig ( el: SystemTemplateElementBase, ): { amount: number; unit: string } | null { const type = String(el.type || '').toUpperCase() const cfg = (el.config || {}) as Record if (type === 'DURATION') { const unit = String(cfg.format ?? cfg.Format ?? 'Days').trim() || 'Days' const u = OFFSET_UNITS.has(unit) ? unit : 'Days' const rawV = cfg.durationValue ?? cfg.value ?? cfg.offsetDays ?? cfg.DurationValue ?? cfg.Value ?? cfg.OffsetDays const val = Number(rawV) return { amount: Number.isFinite(val) ? val : 0, unit: u } } const od = cfg.offsetDays ?? cfg.OffsetDays if (od != null && String(od).trim() !== '') { const n = Number(od) if (Number.isFinite(n)) return { amount: n, unit: 'Days' } } const fmt = String(cfg.format ?? cfg.Format ?? '').trim() if (fmt && OFFSET_UNITS.has(fmt)) { const rawV = cfg.durationValue ?? cfg.value ?? cfg.DurationValue ?? cfg.Value ?? 0 const val = Number(rawV) return { amount: Number.isFinite(val) ? val : 0, unit: fmt } } return null } function resolveFromOffsetAmount ( el: SystemTemplateElementBase, amount: number, unit: string, base: Date, ): string { const type = String(el.type || '').toUpperCase() const cfg = (el.config || {}) as Record const d = applyOffsetToDate(base, amount, unit) if (type === 'DATE') { return applyElementPrefix(cfg, formatDateByPreset(dateFormatPatternForElement(el, cfg), d)) } if (type === 'TIME') { return applyElementPrefix(cfg, formatDateByPreset('HH:mm', d)) } return applyElementPrefix(cfg, `${amount} ${unit}`.trim()) } /** * 画布预览 / 原生打印:按基准时刻解析 DATE/TIME/DURATION,忽略 AUTO_DB 上已过期的 text/format。 */ export function resolveElementDateTimeDisplay ( el: SystemTemplateElementBase, base: Date = new Date(), ): string | null { const type = String(el.type || '').toUpperCase() if (type !== 'DATE' && type !== 'TIME' && type !== 'DURATION') return null const cfg = (el.config || {}) as Record const vst = String(el.valueSourceType ?? '').toUpperCase() const inputType = String(cfg.inputType ?? cfg.InputType ?? '').toLowerCase() const rawText = String(cfg.text ?? cfg.Text ?? '').trim() const fromOffsetJson = (raw: string): string | null => { const p = tryParsePrintInputOffsetStored(raw) if (!p) return null const amount = normalizeOffsetAmount(p.value) if (amount === null) return null return resolveFromOffsetAmount(el, amount, p.unit || 'Days', base) } const jsonResolved = (rawText ? fromOffsetJson(rawText) : null) ?? fromOffsetJson(String(cfg.format ?? cfg.Format ?? '')) if (jsonResolved) return jsonResolved if (vst === 'PRINT_INPUT') { /** 保质期/Prep 等相对偏移字段:须按 base 重算,勿用接口首屏写入的过期 literal */ if ( rawText && !tryParsePrintInputOffsetStored(rawText) && !isUniAppDateTimeOffsetField(el) ) { return applyElementPrefix(cfg, rawText) } if ( isUniAppDateTimeOffsetField(el) || inputType === 'date' || inputType === 'datetime' || type === 'TIME' ) { const off = readOffsetFromElementConfig(el) return resolveFromOffsetAmount(el, off?.amount ?? 0, off?.unit ?? 'Days', base) } return null } const off = readOffsetFromElementConfig(el) if (off) return resolveFromOffsetAmount(el, off.amount, off.unit, base) return resolveFromOffsetAmount(el, 0, 'Days', base) } export function tryParsePrintInputOffsetStored ( raw: string | null | undefined, ): { unit: string; value: string } | null { const t = String(raw ?? '').trim() if (!t.startsWith('{')) return null try { const o = JSON.parse(t) as unknown if (o == null || typeof o !== 'object' || Array.isArray(o)) return null const rec = o as Record const unit = String(rec.unit ?? rec.Unit ?? '').trim() const value = String(rec.value ?? rec.Value ?? '') if (!unit || !OFFSET_UNITS.has(unit)) return null return { unit, value } } catch { return null } } /** 与 Web isDateTimeDataEntryField 对齐(App 侧元素无 typeAdd 时用命名兜底) */ export function isUniAppDateTimeOffsetField (el: SystemTemplateElementBase): boolean { const type = String(el.type || '').toUpperCase() const cfg = (el.config || {}) as Record if (type === 'TIME' || type === 'DURATION') return true if (type !== 'DATE') return false const it = String(cfg.inputType ?? cfg.InputType ?? '').toLowerCase() if (it === 'datetime' || it === 'date') return true const ta = String((el as { typeAdd?: string }).typeAdd ?? '').trim().toLowerCase().replace(/\s+/g, ' ') if (ta === 'label_duration date' || ta.includes('duration date')) return true if (ta === 'auto_current date' || ta.includes('current date')) return true if (ta === 'auto_current time' || ta.includes('current time')) return true const nameHint = `${el.elementName ?? ''} ${el.inputKey ?? ''}`.toLowerCase() if (/(durationdate|duration_date|duration\s*date)/.test(nameHint)) return true return false } /** * 将 templateProductDefaults 里单格字符串转为画布用的展示文案(相对 now)。 * 非 JSON 或无法解析时返回原串(兼容旧版已展开的日期文案)。 */ /** * 预览/出纸前:按同一基准时刻重算所有 DATE/TIME/DURATION 及偏移类字段,保证 Prep 与 Use By 一致。 */ export function applyLiveDateTimeFieldsToTemplate ( template: SystemLabelTemplate, base: Date = new Date(), ): SystemLabelTemplate { const elements = (template.elements || []).map((el) => { const type = String(el.type || '').toUpperCase() const needsLive = type === 'DATE' || type === 'TIME' || type === 'DURATION' || isUniAppDateTimeOffsetField(el) if (!needsLive) return el const live = resolveElementDateTimeDisplay(el, base) if (live == null || !String(live).trim()) return el const cfg = { ...(el.config || {}) } as Record cfg.text = live cfg.Text = live return { ...el, config: cfg } }) return { ...template, elements } } export function resolveTemplateDefaultValueForElement ( el: SystemTemplateElementBase, stored: string, now: Date, ): string { const s = String(stored ?? '').trim() if (!s) return '' if (!isUniAppDateTimeOffsetField(el)) return s const parsed = tryParsePrintInputOffsetStored(s) if (parsed) { const amount = normalizeOffsetAmount(parsed.value) if (amount === null) return '' return resolveFromOffsetAmount(el, amount, parsed.unit || 'Days', now) } if (isLikelyResolvedDateTimeLiteral(s)) { const off = readOffsetFromElementConfig(el) return resolveFromOffsetAmount(el, off?.amount ?? 0, off?.unit ?? 'Days', now) } return s }