import type { LabelElement } from "../types/labelTemplate"; import { canonicalElementType, elementInputKey, isDateTimeDataEntryField, normalizeValueSourceTypeForElement, resolvedTypeAddForPersist, } from "../types/labelTemplate"; /** * 标签创建/编辑表单:相对时间偏移 + 按模板 format 预览(与 LabelCanvas formatDateByPreset 对齐) */ export const LABEL_FORM_OFFSET_UNITS = [ "Minutes", "Hours", "Days", "Weeks", "Months (30 Day)", "Years", ] as const; export type LabelFormOffsetUnit = (typeof LABEL_FORM_OFFSET_UNITS)[number]; 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; } /** 与 LabelCanvas 中 formatDateByPreset 一致 */ export 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 = date.toLocaleString("en-US", { month: "long" }).toUpperCase(); const dayLong = date.toLocaleString("en-US", { weekday: "long" }).toUpperCase(); const dayShort = date.toLocaleString("en-US", { weekday: "short" }).toUpperCase(); const monthShort = date.toLocaleString("en-US", { month: "short" }).toUpperCase(); 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}`; default: return 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); } } const OFFSET_UNIT_SET = new Set(LABEL_FORM_OFFSET_UNITS as unknown as string[]); /** 已算好的日期/时间展示串(非 format 预设名、非 JSON 偏移) */ export function isLikelyResolvedDateTimeLiteral(raw: string | null | undefined): boolean { const t = String(raw ?? "").trim(); if (!t || t.startsWith("{")) return false; if (OFFSET_UNIT_SET.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; } export type NormalizedLabelFormOffset = | { kind: "zero" } | { kind: "amount"; amount: number; storeValue: string } | { kind: "invalid" }; /** * 创建/编辑标签表单:日期时间类偏移的**空串**或**数值 0**(含 0、0.0、+0 等)均视为相对基准偏移 0; * 预览即「当前基准时刻」;落库建议用 {@link serializePrintInputOffset}(unit, "0")。 */ export function normalizeLabelFormOffsetInput(valueRaw: string | undefined): NormalizedLabelFormOffset { const t = String(valueRaw ?? "").trim(); if (t === "") return { kind: "zero" }; const n = Number(t); if (!Number.isFinite(n)) return { kind: "invalid" }; if (n === 0) return { kind: "zero" }; return { kind: "amount", amount: n, storeValue: t }; } /** * 模板 productDefault 中日期/时间/时长录入:存 `{"unit":"Days","value":"2"}`,供 App 与打印按 BaseTime 解析。 */ export function serializePrintInputOffset(unit: string, value: string): string { const u = String(unit ?? "").trim() || "Days"; return JSON.stringify({ unit: u, value: String(value ?? "") }); } 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_UNIT_SET.has(unit)) return null; return { unit, value }; } catch { return null; } } /** 批量录入格:无 JSON 时纯数字按 Days 预填,便于兼容旧 duration 整数 */ export function offsetFieldUiStateFromStored(stored: string | undefined): { unit: string; value: string } { const p = tryParsePrintInputOffsetStored(stored); if (p) return p; const t = String(stored ?? "").trim(); if (/^\d+$/.test(t)) return { unit: "Days", value: t }; return { unit: "Days", value: "" }; } /** * 将落库的 JSON(或旧版纯文案)解析为画布/预览用的展示字符串(相对 base 时间)。 */ export function resolveStoredPrintValueToDisplayText( el: LabelElement, stored: string, base: Date, ): string { const s = String(stored ?? "").trim(); if (!s) return ""; if (!isDateTimeDataEntryField(el)) return s; const parsed = tryParsePrintInputOffsetStored(s); if (!parsed) { if (isLikelyResolvedDateTimeLiteral(s)) { const norm = normalizeLabelFormOffsetInput("0"); const amount = norm.kind === "zero" ? 0 : norm.kind === "amount" ? norm.amount : 0; const unit = "Days"; const d = applyOffsetToDate(base, amount, unit); const type = canonicalElementType(el.type); const cfg = el.config as Record; if (type === "DATE") { const it = String(cfg.inputType ?? cfg.InputType ?? "").toLowerCase(); const format = (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"; const fmt = isLikelyResolvedDateTimeLiteral(format) ? it === "datetime" ? "YYYY-MM-DD HH:mm" : "DD/MM/YYYY" : format; return formatDateByPreset(fmt, d); } if (type === "TIME") return formatDateByPreset("HH:mm", d); } return s; } const unit = parsed.unit || "Days"; const norm = normalizeLabelFormOffsetInput(parsed.value); if (norm.kind === "invalid") return s; const amount = norm.kind === "zero" ? 0 : norm.amount; const type = canonicalElementType(el.type); const d = applyOffsetToDate(base, amount, unit); const cfg = el.config as Record; if (type === "DATE") { const it = String(cfg.inputType ?? cfg.InputType ?? "").toLowerCase(); const format = (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"; return formatDateByPreset(format, d); } if (type === "TIME") { return formatDateByPreset("HH:mm", d); } return `${amount} ${unit}`; } 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 dateFormatPatternForElement(el: LabelElement, cfg: Record): string { const type = canonicalElementType(el.type); 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_UNIT_SET.has(raw)) { return it === "datetime" ? "YYYY-MM-DD HH:mm" : "DD/MM/YYYY"; } return raw; } function readOffsetFromElementConfig(el: LabelElement): { amount: number; unit: string } | null { const type = canonicalElementType(el.type); const cfg = (el.config ?? {}) as Record; if (type === "DURATION") { const unit = String(cfg.format ?? cfg.Format ?? "Days").trim() || "Days"; const u = OFFSET_UNIT_SET.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_UNIT_SET.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: LabelElement, amount: number, unit: string, base: Date, ): string { const type = canonicalElementType(el.type); 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()); } /** 与 App isUniAppDateTimeOffsetField 对齐:含 auto current date/time 等相对基准解析字段 */ export function isDateTimeLiveResolveField(el: LabelElement): boolean { const type = canonicalElementType(el.type); 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 = resolvedTypeAddForPersist(el).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 ?? ""} ${elementInputKey(el)}`.toLowerCase(); if (/(durationdate|duration_date|duration\s*date)/.test(nameHint)) return true; return false; } /** * 画布预览 / 打印:按基准时刻解析 DATE/TIME/DURATION(与 App resolveElementDateTimeDisplay 一致)。 */ export function resolveElementDateTimeDisplay( el: LabelElement, base: Date = new Date(), ): string | null { const type = canonicalElementType(el.type); if (type !== "DATE" && type !== "TIME" && type !== "DURATION") return null; const cfg = (el.config ?? {}) as Record; const vst = normalizeValueSourceTypeForElement(el); 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 norm = normalizeLabelFormOffsetInput(p.value); if (norm.kind === "invalid") return null; const amount = norm.kind === "zero" ? 0 : norm.amount; 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") { if ( rawText && !tryParsePrintInputOffsetStored(rawText) && !isDateTimeLiveResolveField(el) ) { return applyElementPrefix(cfg, rawText); } if ( isDateTimeLiveResolveField(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); } /** 预览前:按同一基准时刻重算所有 DATE/TIME/DURATION,写入 __previewFormatted */ export function applyLiveDateTimePreviewToElements( elements: LabelElement[], base: Date = new Date(), ): LabelElement[] { return elements.map((el) => { const type = canonicalElementType(el.type); const needsLive = type === "DATE" || type === "TIME" || type === "DURATION" || isDateTimeLiveResolveField(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.__previewFormatted = live; return { ...el, config: cfg }; }); }