/** * 与 Web 管理端 labelFormDatePreview 一致:templateProductDefaults 中日期/时间/时长存 JSON * `{"unit":"Days","value":"2"}`,预览与打印时按 BaseTime 解析为 config.text。 */ import type { SystemLabelTemplate, SystemTemplateElementBase } from '../print/types/printer' const OFFSET_UNITS = new Set([ 'Minutes', 'Hours', 'Days', 'Weeks', 'Months (30 Day)', 'Years', ]) /** 与 Web PropertiesPanel DATE_FORMAT_OPTIONS + 常用 datetime 预设一致 */ export const DATE_DISPLAY_FORMAT_PRESETS = new Set([ 'DD/MM/YYYY', 'MM/DD/YYYY', 'DD/MM/YY', 'MM/DD/YY', 'MM/YY', 'MM/DD', 'MM', 'DD', 'YY', 'FULLY DAY(WEDNESDAY)', 'DAY (WED)', 'MONTH (DECEMBER)', 'YEAR (2025)', 'DD MONTH YEAR (25 DECEMBER 2025)', 'YYYY-MM-DD', 'YYYY-MM-DD HH:mm', 'HH:mm', ]) export function isKnownDateDisplayFormat (raw: string | null | undefined): boolean { return DATE_DISPLAY_FORMAT_PRESETS.has(String(raw ?? '').trim()) } export function isStoredPrintInputOffsetPayload (raw: string | null | undefined): boolean { return parseStoredPrintInputOffset(String(raw ?? '').trim()) != null } /** 自定义 datetime 模板(非预设名),不能作为展示 format */ function looksLikeCustomDateTimeFormatTemplate (raw: string | null | undefined): boolean { const t = String(raw ?? '').trim() if (!t || isKnownDateDisplayFormat(t)) return false if (/GMT/i.test(t)) return true if (/\bCST\b|\bMT\+|\(CST\)/i.test(t)) return true if (/HH:mm:ss/i.test(t)) return true if (/:\d{2}:\d{2}/.test(t)) return true if (/^\d{1,2}\s+\d{4}\s+\d{1,2}:\d{2}/.test(t)) return true return false } /** 已展开的日期/时间乱串(GMT 等);**不含**合法的偏移 JSON */ export function isCorruptedDateDisplayFormat (raw: string | null | undefined): boolean { const t = String(raw ?? '').trim() if (!t) return false if (isStoredPrintInputOffsetPayload(t)) return false if (OFFSET_UNITS.has(t)) return true if (looksLikeCustomDateTimeFormatTemplate(t)) return true /** 如 06 2026 13:45:57 GMT+0 */ if (/^\d{1,2}\s+\d{4}\s+\d{1,2}:\d{2}(:\d{2})?(\s+GMT)?/i.test(t)) return true /** App 端 toLocaleString 失败时的 Date.toString():MON JUL 06 2026 14:29:07 GMT+0800 (CST) */ if (/^(SUN|MON|TUE|WED|THU|FRI|SAT)\s+[A-Z]{3}\s+\d{1,2}\s+\d{4}/i.test(t)) return true return false } /** 与 normalizeLabelPanelTypeAdd 一致:优先元素 typeAdd,其次 config.typeAdd */ export function resolveElementTypeAdd (el: SystemTemplateElementBase): string { const cfg = (el.config || {}) as Record return String( (el as { typeAdd?: string }).typeAdd ?? cfg.typeAdd ?? cfg.TypeAdd ?? '', ).trim() } /** 元素命名/id hint(用于区分 durationdate / durationdate2) */ export function elementDateNameHint (el: SystemTemplateElementBase): string { return `${el.elementName ?? ''} ${el.inputKey ?? ''} ${el.id ?? ''}`.toLowerCase() } /** 底部黑条星期:仅 durationdate2(**不含** Use By 的 durationdate) */ export function isWeekdayBarNameHint (hint: string): boolean { return /durationdate2|duration_date_2|durationdate_2|duration\s*date\s*2/.test(hint) } /** Use By / 到期日:durationdate(无 2)、use by 等 */ export function isUseByDateNameHint (hint: string): boolean { if (isWeekdayBarNameHint(hint)) return false return ( /(?:^|\s|_)durationdate(?:\s|_|$)/.test(hint) || /duration\s*date(?!\s*2)/.test(hint) || /duration_date(?!_2)/.test(hint) || /use\s*by|must\s*use|expir/.test(hint) ) } function frozenKnownDateFormat (cfg: Record): string { const preserved = String(cfg.__dateDisplayFormat ?? cfg.__DateDisplayFormat ?? '').trim() if (preserved && isKnownDateDisplayFormat(preserved)) return preserved for (const c of [cfg.format, cfg.Format, cfg.displayFormat, cfg.DisplayFormat]) { const raw = String(c ?? '').trim() if (raw && isKnownDateDisplayFormat(raw) && !parseStoredPrintInputOffset(raw)) return raw } return '' } /** 打印时输入的字面量字段(Number/Text/Weight),不得按日期偏移重算 */ export function isPrintInputLiteralField (el: SystemTemplateElementBase): boolean { const vst = String(el.valueSourceType || '').toUpperCase() if (vst !== 'PRINT_INPUT') return false const rawType = String(el.type || '').trim() const typeUpper = rawType.toUpperCase() if (typeUpper === 'WEIGHT' || typeUpper.includes('|WEIGHT')) return true const cfg = (el.config || {}) as Record const it = String(cfg.inputType ?? cfg.InputType ?? '').toLowerCase() if (it === 'number' || it === 'text') return true const ta = resolveElementTypeAdd(el).toLowerCase() if (/\|\s*number\b/.test(ta) || ta.endsWith('number')) return true if (/\|\s*weight\b/.test(ta) || ta.endsWith('weight')) return true if (/\|\s*text\b/.test(ta) || ta.endsWith('|text')) return true if (/print[\s|]*number/i.test(rawType)) return true if (/print[\s|]*text/i.test(rawType)) return true if (/print[\s|]*weight/i.test(rawType)) return true return false } /** 打印时输入 Number/Text/Weight 的展示文案(与 Web 画布 print|number 一致) */ export function resolvePrintInputLiteralDisplay (el: SystemTemplateElementBase): string { const cfg = (el.config || {}) as Record const it = String(cfg.inputType ?? cfg.InputType ?? '').toLowerCase() const type = String(el.type || '').toUpperCase() let body = '' if (type === 'WEIGHT') { body = String(cfg.value ?? cfg.Value ?? '').trim() if (!body) body = String(cfg.text ?? cfg.Text ?? '').trim() } else { body = String(cfg.text ?? cfg.Text ?? '').trim() } if (!body && it === 'number') body = '0' /** 字面量 Number/Text 的纯数字不能走偏移 JSON 解析(否则会误当成 Days 偏移而清空) */ if (isCorruptedDateDisplayFormat(body)) return '' const unit = String(cfg.unit ?? cfg.Unit ?? '').trim() if (unit && body && !body.endsWith(unit)) body = `${body}${unit}` return applyElementPrefix(cfg, body) } /** Text (For Template) 等固定文案:invertColors 仅表示黑底白字,不是星期条 */ export function isFixedStaticTextElement (el: SystemTemplateElementBase): boolean { if (isPrintInputLiteralField(el)) return true const type = String(el.type || '').toUpperCase() if (type !== 'TEXT_STATIC' && type !== 'TEXT' && type !== 'TEXT_PRODUCT' && type !== 'TEXT_PRICE') { return false } const vst = String(el.valueSourceType || '').toUpperCase() if (vst && vst !== 'FIXED' && vst !== 'STATIC') return false const cfg = (el.config || {}) as Record const inputType = String(cfg.inputType ?? cfg.InputType ?? '').toLowerCase() if ( inputType === 'date' || inputType === 'datetime' || inputType === 'number' || inputType === 'options' ) { return false } if (isUniAppDateTimeOffsetField(el)) return false if (isWeekdayBarNameHint(elementDateNameHint(el))) return false return true } /** * 底部黑条星期元素(durationdate2 / FULLY DAY 格式)。 * invertColors 只负责黑底白字样式,不能单独判定为星期条。 */ export function isWeekdayBarElement (el: SystemTemplateElementBase): boolean { const type = String(el.type || '').toUpperCase() /** 重打已焙成 TEXT_STATIC 的星期条不再走 live 偏移,直接画 config.text */ if (type === 'TEXT_STATIC' || type === 'TEXT') return false if (isFixedStaticTextElement(el)) return false const cfg = (el.config || {}) as Record const hint = elementDateNameHint(el) if (isWeekdayBarNameHint(hint)) return true const fmt = frozenKnownDateFormat(cfg) if (fmt === 'FULLY DAY(WEDNESDAY)' || fmt === 'DAY (WED)') return true return false } /** @deprecated 使用 isWeekdayBarElement */ export function isInvertedWeekdayElement (el: SystemTemplateElementBase): boolean { return isWeekdayBarElement(el) } /** 首次解析模板时冻结展示用 format,避免后续 merge 把 offset JSON / 展开串写进 format 后丢失 FULLY DAY 等 */ export function captureDateDisplayFormatInConfig (cfg: Record): void { if (String(cfg.__dateDisplayFormat ?? cfg.__DateDisplayFormat ?? '').trim()) return for (const c of [cfg.format, cfg.Format, cfg.displayFormat, cfg.DisplayFormat]) { const raw = String(c ?? '').trim() if (!raw || parseStoredPrintInputOffset(raw)) continue if (!isKnownDateDisplayFormat(raw) || isCorruptedDateDisplayFormat(raw)) continue cfg.__dateDisplayFormat = raw return } } /** 清理被污染的 format/text,恢复 FULLY DAY 等展示格式;保留合法偏移 JSON */ export function sanitizeDateElementConfig ( el: SystemTemplateElementBase, cfg: Record, ): void { if (isFixedStaticTextElement(el)) return captureDateDisplayFormatInConfig(cfg) const preserved = String(cfg.__dateDisplayFormat ?? cfg.__DateDisplayFormat ?? '').trim() let fmt = String(cfg.format ?? cfg.Format ?? '').trim() let txt = String(cfg.text ?? cfg.Text ?? '').trim() const fmtOffset = isStoredPrintInputOffsetPayload(fmt) const txtOffset = isStoredPrintInputOffsetPayload(txt) /** 后端 PRINT_INPUT 可能把偏移 JSON 写进 format;统一迁到 text 并恢复展示 format */ if (fmtOffset && !txtOffset) { cfg.text = fmt cfg.Text = fmt txt = fmt } if (fmtOffset || txtOffset) { const hint = elementDateNameHint(el) const next = (preserved && isKnownDateDisplayFormat(preserved) ? preserved : '') || (isWeekdayBarNameHint(hint) ? 'FULLY DAY(WEDNESDAY)' : '') || (isUseByDateNameHint(hint) ? 'MM/DD/YYYY' : '') || 'DD/MM/YYYY' if (isKnownDateDisplayFormat(next)) { cfg.format = next cfg.Format = next if (!cfg.__dateDisplayFormat) cfg.__dateDisplayFormat = next } fmt = String(cfg.format ?? cfg.Format ?? '').trim() } else if (fmt && (isCorruptedDateDisplayFormat(fmt) || looksLikeCustomDateTimeFormatTemplate(fmt))) { if (isFixedStaticTextElement(el)) return const hint = elementDateNameHint(el) const next = preserved || (isWeekdayBarNameHint(hint) ? 'FULLY DAY(WEDNESDAY)' : '') || (isUseByDateNameHint(hint) ? 'MM/DD/YYYY' : '') if (next) { cfg.format = next cfg.Format = next if (!cfg.__dateDisplayFormat) cfg.__dateDisplayFormat = next } else { delete cfg.format delete cfg.Format } } const txtAfter = String(cfg.text ?? cfg.Text ?? '').trim() if (txtAfter && isCorruptedDateDisplayFormat(txtAfter)) { const hint = elementDateNameHint(el) if (isUniAppDateTimeOffsetField(el) || isWeekdayBarNameHint(hint) || isUseByDateNameHint(hint)) { delete cfg.text delete cfg.Text } } const pf = String(cfg.__previewFormatted ?? cfg.__PreviewFormatted ?? '').trim() if (pf && isCorruptedDateDisplayFormat(pf)) { delete cfg.__previewFormatted delete cfg.__PreviewFormatted } } 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 } /** App 原生端 toLocaleString(weekday) 常返回整段 Date.toString(),须用手动表 */ const WEEKDAY_LONG_EN = [ 'SUNDAY', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', ] as const const WEEKDAY_SHORT_EN = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'] as const /** 已是展开后的星期展示文案(重打快照 / 落库 __previewFormatted) */ export function isResolvedWeekdayDisplayLiteral (raw: string | null | undefined): boolean { const t = String(raw ?? '').trim().toUpperCase() if (!t) return false if ((WEEKDAY_LONG_EN as readonly string[]).includes(t)) return true if ((WEEKDAY_SHORT_EN as readonly string[]).includes(t)) return true if (/^DAY\s*\([A-Z]{3}\)$/i.test(String(raw ?? '').trim())) return true return false } /** 重打/画布:优先读快照里已冻结的星期文案,禁止再按 today 重算 */ export function readFrozenWeekdayDisplayFromConfig (cfg: Record): string { for (const key of ['__previewFormatted', '__PreviewFormatted', 'text', 'Text'] as const) { const raw = String(cfg[key] ?? '').trim() if (!raw) continue if (parseStoredPrintInputOffset(raw)) continue if (isCorruptedDateDisplayFormat(raw)) continue if (isResolvedWeekdayDisplayLiteral(raw)) { return raw.toUpperCase().startsWith('DAY (') ? raw : raw.toUpperCase() } } return '' } const MONTH_LONG_EN = [ 'JANUARY', 'FEBRUARY', 'MARCH', 'APRIL', 'MAY', 'JUNE', 'JULY', 'AUGUST', 'SEPTEMBER', 'OCTOBER', 'NOVEMBER', 'DECEMBER', ] as const function weekdayLongEn (date: Date): string { return WEEKDAY_LONG_EN[date.getDay()] ?? 'SUNDAY' } function weekdayShortEn (date: Date): string { return WEEKDAY_SHORT_EN[date.getDay()] ?? 'SUN' } function monthLongEn (date: Date): string { return MONTH_LONG_EN[date.getMonth()] ?? 'JANUARY' } 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') const monthLong = monthLongEn(date) const dayLong = weekdayLongEn(date) const dayShort = weekdayShortEn(date) 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 'FULLY DAY(WEDNESDAY)': return dayLong case 'DAY (WED)': return dayShort case 'MONTH (DECEMBER)': return monthLong case 'YEAR (2025)': return yyyy case 'DD MONTH YEAR (25 DECEMBER 2025)': return `${dd} ${monthLong} ${yyyy}` 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: if (isCorruptedDateDisplayFormat(format) || looksLikeCustomDateTimeFormatTemplate(format)) { return `${mm}/${dd}/${yyyy}` } 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 inferDurationWeekdayDisplayFormat (el: SystemTemplateElementBase, cfg: Record): string | null { if (!isWeekdayBarElement({ ...el, config: cfg })) return null const preserved = String(cfg.__dateDisplayFormat ?? cfg.__DateDisplayFormat ?? '').trim() if (preserved === 'FULLY DAY(WEDNESDAY)' || preserved === 'DAY (WED)') return preserved return 'FULLY DAY(WEDNESDAY)' } function dateFormatPatternForElement ( el: SystemTemplateElementBase, cfg: Record, ): string { const type = String(el.type || '').toUpperCase() if (type === 'TIME') return 'HH:mm' /** 仅底部黑条星期走 FULLY DAY;Use By 仍用 MM/DD/YYYY */ if (isWeekdayBarElement({ ...el, config: cfg })) { const preserved = String(cfg.__dateDisplayFormat ?? cfg.__DateDisplayFormat ?? '').trim() if (preserved === 'FULLY DAY(WEDNESDAY)' || preserved === 'DAY (WED)') return preserved return 'FULLY DAY(WEDNESDAY)' } const it = String(cfg.inputType ?? cfg.InputType ?? '').toLowerCase() const preserved = String(cfg.__dateDisplayFormat ?? cfg.__DateDisplayFormat ?? '').trim() if (preserved && isKnownDateDisplayFormat(preserved)) return preserved const hint = elementDateNameHint(el) if (isUseByDateNameHint(hint)) return 'MM/DD/YYYY' const candidates = [ cfg.format, cfg.Format, cfg.displayFormat, cfg.DisplayFormat, cfg.dateFormat, cfg.DateFormat, ] .map((v) => (typeof v === 'string' ? v.trim() : '')) .filter(Boolean) for (const raw of candidates) { if (isStoredPrintInputOffsetPayload(raw)) continue if (isCorruptedDateDisplayFormat(raw) || looksLikeCustomDateTimeFormatTemplate(raw)) continue if (isLikelyResolvedDateTimeLiteral(raw) || OFFSET_UNITS.has(raw)) continue if (isKnownDateDisplayFormat(raw)) return raw } const inferred = inferDurationWeekdayDisplayFormat(el, cfg) if (inferred) return inferred return it === 'datetime' ? 'YYYY-MM-DD HH:mm' : 'DD/MM/YYYY' } function readOffsetFromElementConfig ( el: SystemTemplateElementBase, ): { amount: number; unit: string } | null { const type = String(el.type || '').toUpperCase() const cfg = (el.config || {}) as Record const fromStoredText = parseStoredPrintInputOffset(String(cfg.text ?? cfg.Text ?? '')) if (fromStoredText) { const amount = normalizeOffsetAmount(fromStoredText.value) if (amount !== null) { return { amount, unit: fromStoredText.unit || 'Days' } } } const fromStoredFormat = parseStoredPrintInputOffset(String(cfg.format ?? cfg.Format ?? '')) if (fromStoredFormat) { const amount = normalizeOffsetAmount(fromStoredFormat.value) if (amount !== null) { return { amount, unit: fromStoredFormat.unit || 'Days' } } } 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 sanitizeDateElementConfig(el, cfg) if (isPrintInputLiteralField({ ...el, config: cfg })) { const line = resolvePrintInputLiteralDisplay({ ...el, config: cfg }) return line || null } 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 = parseStoredPrintInputOffset(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) { if (isCorruptedDateDisplayFormat(jsonResolved)) { if (isWeekdayBarElement({ ...el, config: cfg })) { return resolveWeekdayDisplayForElement({ ...el, config: cfg }, base) } return null } return jsonResolved } const isOffsetField = isUniAppDateTimeOffsetField({ ...el, config: cfg }) if (vst === 'PRINT_INPUT') { /** 非相对偏移的 PRINT_INPUT 才使用固定文案;勿把已展开的 GMT/时间串当最终展示 */ if ( rawText && !parseStoredPrintInputOffset(rawText) && !isOffsetField && isLikelyResolvedDateTimeLiteral(rawText) && !isCorruptedDateDisplayFormat(rawText) ) { return applyElementPrefix(cfg, rawText) } if ( isOffsetField || inputType === 'date' || inputType === 'datetime' || type === 'TIME' ) { const off = readOffsetFromElementConfig({ ...el, config: cfg }) if (off) return resolveFromOffsetAmount({ ...el, config: cfg }, off.amount, off.unit, base) return resolveFromOffsetAmount({ ...el, config: cfg }, 0, 'Days', base) } return null } /** FIXED 的 durationdate/durationtime 等:config.text 可能已是平台录入的偏移 JSON / 纯数字 */ if (vst === 'FIXED' && isOffsetField) { const off = readOffsetFromElementConfig({ ...el, config: cfg }) if (off) return resolveFromOffsetAmount({ ...el, config: cfg }, off.amount, off.unit, base) return resolveFromOffsetAmount({ ...el, config: cfg }, 0, 'Days', base) } if (vst === 'AUTO_DB' && (type === 'DATE' || type === 'TIME' || type === 'DURATION' || isOffsetField)) { const off = readOffsetFromElementConfig({ ...el, config: cfg }) if (off) return resolveFromOffsetAmount({ ...el, config: cfg }, off.amount, off.unit, base) return resolveFromOffsetAmount({ ...el, config: cfg }, 0, 'Days', base) } const off = readOffsetFromElementConfig({ ...el, config: cfg }) if (off) return resolveFromOffsetAmount({ ...el, config: cfg }, off.amount, off.unit, base) if (isOffsetField) { return resolveFromOffsetAmount({ ...el, config: cfg }, 0, 'Days', base) } if ( rawText && !parseStoredPrintInputOffset(rawText) && isLikelyResolvedDateTimeLiteral(rawText) ) { return applyElementPrefix(cfg, rawText) } return resolveFromOffsetAmount({ ...el, config: cfg }, 0, 'Days', base) } /** 底部黑条星期(durationdate2);Use By 的 durationdate 不在此列 */ export function resolveWeekdayDisplayForElement ( el: SystemTemplateElementBase, base: Date = new Date(), ): string { const cfg = { ...(el.config || {}) } as Record sanitizeDateElementConfig(el, cfg) const frozen = readFrozenWeekdayDisplayFromConfig(cfg) if (frozen) return applyElementPrefix(cfg, frozen) const off = readOffsetFromElementConfig({ ...el, config: cfg }) const amount = off?.amount ?? 0 const unit = off?.unit ?? 'Days' const d = applyOffsetToDate(base, amount, unit) const preserved = String(cfg.__dateDisplayFormat ?? cfg.__DateDisplayFormat ?? '').trim() const pattern = preserved === 'DAY (WED)' ? 'DAY (WED)' : 'FULLY DAY(WEDNESDAY)' return applyElementPrefix(cfg, formatDateByPreset(pattern, d)) } export function tryParsePrintInputOffsetStored ( raw: string | null | undefined, ): { unit: string; value: string } | null { const t = String(raw ?? '').trim() if (!t) return null if (t.startsWith('{')) { 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 } } /** 兼容旧版 templateProductDefaults 仅存纯数字(表示 Days) */ if (/^\d+$/.test(t)) return { unit: 'Days', value: t } return null } /** 与 Web offsetFieldUiStateFromStored 对齐 */ export function parseStoredPrintInputOffset ( raw: string | null | undefined, ): { unit: string; value: string } | null { return tryParsePrintInputOffsetStored(raw) } /** 与 Web isDateTimeLiveResolveField 对齐:含 auto current date/time 等相对基准解析字段 */ export function isUniAppDateTimeOffsetField (el: SystemTemplateElementBase): boolean { if (isPrintInputLiteralField(el)) return false 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 = resolveElementTypeAdd(el).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 cfg = { ...(el.config || {}) } as Record const weekdayEl = isWeekdayBarElement(el) if (isPrintInputLiteralField(el)) return el const needsLive = type === 'DATE' || type === 'TIME' || type === 'DURATION' || isUniAppDateTimeOffsetField(el) || weekdayEl if (!needsLive) return el sanitizeDateElementConfig(el, cfg) /** 仅 durationdate2 黑条:偏移 + FULLY DAY */ if (weekdayEl) { const live = resolveWeekdayDisplayForElement({ ...el, config: cfg }, base) if (!live || !String(live).trim() || isCorruptedDateDisplayFormat(live)) return el const raw = String(cfg.text ?? cfg.Text ?? '').trim() const keepsOffsetStorage = isUniAppDateTimeOffsetField(el) && (parseStoredPrintInputOffset(raw) != null || /^\d+$/.test(raw)) cfg.__previewFormatted = live if (!keepsOffsetStorage) { cfg.text = live cfg.Text = live } return { ...el, config: cfg } } let live: string | null = null try { live = resolveElementDateTimeDisplay({ ...el, config: cfg }, base) } catch (err) { console.warn('[printInputOffset] applyLiveDateTimeFields resolve failed', el.id, err) } if (live != null && isCorruptedDateDisplayFormat(live)) { live = null } if (live == null || !String(live).trim()) return el const raw = String(cfg.text ?? cfg.Text ?? '').trim() const keepsOffsetStorage = isUniAppDateTimeOffsetField(el) && (parseStoredPrintInputOffset(raw) != null || /^\d+$/.test(raw)) cfg.__previewFormatted = live if (!keepsOffsetStorage) { 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 = parseStoredPrintInputOffset(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) if (off) return resolveFromOffsetAmount(el, off.amount, off.unit, now) } return s }