From 40929347cadf2dac8f0d1dfc7e05f674bfb26440 Mon Sep 17 00:00:00 2001 From: jokerxue <2509699647@qq.com> Date: Thu, 9 Jul 2026 17:46:43 +0800 Subject: [PATCH] feat: update label bulk add and template rotation --- 美国版/Food Labeling Management App UniApp/src/utils/labelPreview/normalizePreviewTemplate.ts | 2 +- 美国版/Food Labeling Management App UniApp/src/utils/labelPreview/printInputOffset.ts | 16 +++++++++++++--- 美国版/Food Labeling Management App UniApp/src/utils/labelPreview/renderLabelPreviewCanvas.ts | 15 +++++++++++---- 美国版/Food Labeling Management App UniApp/src/utils/print/nativeTemplateElementSupport.ts | 2 +- 美国版/Food Labeling Management App UniApp/src/utils/print/systemTemplateAdapter.ts | 6 +++++- 美国版/Food Labeling Management App UniApp/src/utils/print/types/printer.ts | 2 +- 美国版/Food Labeling Management App UniApp/src/utils/textElementLayout.ts | 17 +++++++++++++++-- 美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/LabelCanvas.tsx | 33 ++++++++++++++++++++++++--------- 美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/PropertiesPanel.tsx | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------- 美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/index.tsx | 3 ++- 美国版/Food Labeling Management Platform/src/components/labels/LabelsList.tsx | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------- 美国版/Food Labeling Management Platform/src/lib/labelFormDatePreview.ts | 21 +++++++++++++++------ 美国版/Food Labeling Management Platform/src/services/labelService.ts | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management Platform/src/types/label.ts | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management Platform/src/types/labelTemplate.ts | 4 ++-- 美国版/Food Labeling Management Platform/src/utils/textElementLayout.ts | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-------- 16 files changed, 400 insertions(+), 81 deletions(-) diff --git a/美国版/Food Labeling Management App UniApp/src/utils/labelPreview/normalizePreviewTemplate.ts b/美国版/Food Labeling Management App UniApp/src/utils/labelPreview/normalizePreviewTemplate.ts index bde57b1..64f0231 100644 --- a/美国版/Food Labeling Management App UniApp/src/utils/labelPreview/normalizePreviewTemplate.ts +++ b/美国版/Food Labeling Management App UniApp/src/utils/labelPreview/normalizePreviewTemplate.ts @@ -691,7 +691,7 @@ export function normalizeLabelTemplateFromPreviewApi(payload: unknown): SystemLa y: Number(e.y ?? e.posY ?? e.PosY ?? 0), width: Number(e.width ?? e.Width ?? 0), height: Number(e.height ?? e.Height ?? 0), - rotation: String(e.rotation ?? e.Rotation ?? 'horizontal') as 'horizontal' | 'vertical', + rotation: String(e.rotation ?? e.Rotation ?? 'horizontal') as 'horizontal' | 'vertical' | 'left' | 'right' | 'rotate180', border: String(e.border ?? e.Border ?? e.BorderType ?? e.borderType ?? 'none'), config: cfg as Record, zIndex: Number(e.zIndex ?? e.ZIndex ?? 0), diff --git a/美国版/Food Labeling Management App UniApp/src/utils/labelPreview/printInputOffset.ts b/美国版/Food Labeling Management App UniApp/src/utils/labelPreview/printInputOffset.ts index c433ae8..ba5169c 100644 --- a/美国版/Food Labeling Management App UniApp/src/utils/labelPreview/printInputOffset.ts +++ b/美国版/Food Labeling Management App UniApp/src/utils/labelPreview/printInputOffset.ts @@ -32,6 +32,8 @@ export const DATE_DISPLAY_FORMAT_PRESETS = new Set([ 'YYYY-MM-DD', 'YYYY-MM-DD HH:mm', 'HH:mm', + '12 hr', + '24 hr', ]) export function isKnownDateDisplayFormat (raw: string | null | undefined): boolean { @@ -372,6 +374,8 @@ function formatDateByPreset (format: string, date: Date): string { 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 hour12 = date.getHours() % 12 || 12 + const ampm = date.getHours() >= 12 ? 'pm' : 'am' const monthLong = monthLongEn(date) const dayLong = weekdayLongEn(date) const dayShort = weekdayShortEn(date) @@ -408,6 +412,9 @@ function formatDateByPreset (format: string, date: Date): string { return `${yyyy}-${mm}-${dd}` case 'YYYY-MM-DD HH:mm': return `${yyyy}-${mm}-${dd} ${hh}:${min}` + case '12 hr': + return `${hour12}:${min}${ampm}` + case '24 hr': case 'HH:mm': return `${hh}:${min}` default: @@ -432,7 +439,7 @@ export function isLikelyResolvedDateTimeLiteral (raw: string | null | undefined) 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 + if (/^\d{1,2}:\d{2}(:\d{2})?(\s?[ap]m)?$/i.test(t)) return true return false } @@ -462,7 +469,10 @@ function dateFormatPatternForElement ( cfg: Record, ): string { const type = String(el.type || '').toUpperCase() - if (type === 'TIME') return 'HH:mm' + if (type === 'TIME') { + const raw = String(cfg.format ?? cfg.Format ?? '24 hr').trim() + return raw === '12 hr' ? '12 hr' : '24 hr' + } /** 仅底部黑条星期走 FULLY DAY;Use By 仍用 MM/DD/YYYY */ if (isWeekdayBarElement({ ...el, config: cfg })) { @@ -561,7 +571,7 @@ function resolveFromOffsetAmount ( return applyElementPrefix(cfg, formatDateByPreset(dateFormatPatternForElement(el, cfg), d)) } if (type === 'TIME') { - return applyElementPrefix(cfg, formatDateByPreset('HH:mm', d)) + return applyElementPrefix(cfg, formatDateByPreset(dateFormatPatternForElement(el, cfg), d)) } return applyElementPrefix(cfg, `${amount} ${unit}`.trim()) } diff --git a/美国版/Food Labeling Management App UniApp/src/utils/labelPreview/renderLabelPreviewCanvas.ts b/美国版/Food Labeling Management App UniApp/src/utils/labelPreview/renderLabelPreviewCanvas.ts index 5c7f0ec..02bce99 100644 --- a/美国版/Food Labeling Management App UniApp/src/utils/labelPreview/renderLabelPreviewCanvas.ts +++ b/美国版/Food Labeling Management App UniApp/src/utils/labelPreview/renderLabelPreviewCanvas.ts @@ -30,6 +30,7 @@ import { readFontWeight, readTextDecoration, readElementRotation, + elementRotationDegrees, isShortSingleLineTextBox, type TextVerticalAlign, } from '../textElementLayout' @@ -1233,7 +1234,7 @@ function runLabelPreviewCanvasDraw( if (!finalText && weekdayBar) { finalText = resolveWeekdayDisplayForElement(el, baseTime) } - const rotation = String(el.rotation ?? (el as any).Rotation ?? 'horizontal').toLowerCase() + const rotationDegrees = elementRotationDegrees(el as any) const drawAt = (bx: number, by: number, bw: number, bh: number) => { const anyCtx = ctx as any if (typeof anyCtx.save === 'function') anyCtx.save() @@ -1353,13 +1354,19 @@ function runLabelPreviewCanvasDraw( if (typeof anyCtx.restore === 'function') anyCtx.restore() } - if (rotation === 'vertical') { + if (rotationDegrees !== 0) { const anyCtx = ctx as any if (typeof anyCtx.save === 'function' && typeof anyCtx.rotate === 'function') { + const isQuarterTurn = rotationDegrees === -90 || rotationDegrees === 90 anyCtx.save() anyCtx.translate(x + w / 2, y + h / 2) - anyCtx.rotate(-Math.PI / 2) - drawAt(-h / 2, -w / 2, h, w) + anyCtx.rotate((rotationDegrees * Math.PI) / 180) + drawAt( + isQuarterTurn ? -h / 2 : -w / 2, + isQuarterTurn ? -w / 2 : -h / 2, + isQuarterTurn ? h : w, + isQuarterTurn ? w : h, + ) anyCtx.restore() } else { drawAt(x, y, w, h) diff --git a/美国版/Food Labeling Management App UniApp/src/utils/print/nativeTemplateElementSupport.ts b/美国版/Food Labeling Management App UniApp/src/utils/print/nativeTemplateElementSupport.ts index 97b8316..6699fbd 100644 --- a/美国版/Food Labeling Management App UniApp/src/utils/print/nativeTemplateElementSupport.ts +++ b/美国版/Food Labeling Management App UniApp/src/utils/print/nativeTemplateElementSupport.ts @@ -244,7 +244,7 @@ export function templateRequiresCanvasStyleFidelity (template: SystemLabelTempla if (border === 'line' || border === 'dotted' || border === 'solid') { return true } - if (readElementRotation(el) === 'vertical') { + if (readElementRotation(el) !== 'horizontal') { return true } const cfg = (el.config || {}) as Record diff --git a/美国版/Food Labeling Management App UniApp/src/utils/print/systemTemplateAdapter.ts b/美国版/Food Labeling Management App UniApp/src/utils/print/systemTemplateAdapter.ts index 96bb832..efa4378 100644 --- a/美国版/Food Labeling Management App UniApp/src/utils/print/systemTemplateAdapter.ts +++ b/美国版/Food Labeling Management App UniApp/src/utils/print/systemTemplateAdapter.ts @@ -261,7 +261,11 @@ function toEscAlign (align: SystemTemplateTextAlign): 0 | 1 | 2 { } function resolveRotation (value?: string): number { - return value === 'vertical' ? 90 : 0 + const rotation = String(value || 'horizontal').trim().toLowerCase() + if (rotation === 'vertical' || rotation === 'left' || rotation === 'rotate-left' || rotation === 'left90') return 90 + if (rotation === 'right' || rotation === 'rotate-right' || rotation === 'right90') return 270 + if (rotation === 'rotate180' || rotation === '180' || rotation === 'down' || rotation === 'inverted') return 180 + return 0 } function normalizeQrLevel (value?: string): 'L' | 'M' | 'Q' | 'H' { diff --git a/美国版/Food Labeling Management App UniApp/src/utils/print/types/printer.ts b/美国版/Food Labeling Management App UniApp/src/utils/print/types/printer.ts index cff5e63..ea743ab 100644 --- a/美国版/Food Labeling Management App UniApp/src/utils/print/types/printer.ts +++ b/美国版/Food Labeling Management App UniApp/src/utils/print/types/printer.ts @@ -68,7 +68,7 @@ export interface RawImageDataSource { export type LabelTemplateValue = string | number export type LabelTemplateData = Record export type PrinterTemplateUnit = 'inch' | 'mm' | 'cm' | 'px' -export type SystemTemplateRotation = 'horizontal' | 'vertical' +export type SystemTemplateRotation = 'horizontal' | 'vertical' | 'left' | 'right' | 'rotate180' export type SystemTemplatePrintOrientation = 'horizontal' | 'vertical' export type SystemTemplateTextAlign = 'left' | 'center' | 'right' diff --git a/美国版/Food Labeling Management App UniApp/src/utils/textElementLayout.ts b/美国版/Food Labeling Management App UniApp/src/utils/textElementLayout.ts index fe0649a..e8c84e6 100644 --- a/美国版/Food Labeling Management App UniApp/src/utils/textElementLayout.ts +++ b/美国版/Food Labeling Management App UniApp/src/utils/textElementLayout.ts @@ -61,9 +61,22 @@ export function readElementBorder( /** 元素 rotation(兼容大小写) */ export function readElementRotation( el: { rotation?: string | null; Rotation?: string | null }, -): 'horizontal' | 'vertical' { +): 'horizontal' | 'vertical' | 'right' | 'rotate180' { const v = String(el.rotation ?? el.Rotation ?? 'horizontal').trim().toLowerCase() - return v === 'vertical' ? 'vertical' : 'horizontal' + if (v === 'vertical' || v === 'left' || v === 'rotate-left' || v === 'left90') return 'vertical' + if (v === 'right' || v === 'rotate-right' || v === 'right90') return 'right' + if (v === 'rotate180' || v === '180' || v === 'down' || v === 'inverted') return 'rotate180' + return 'horizontal' +} + +export function elementRotationDegrees( + el: { rotation?: string | null; Rotation?: string | null }, +): 0 | -90 | 90 | 180 { + const rotation = readElementRotation(el) + if (rotation === 'vertical') return -90 + if (rotation === 'right') return 90 + if (rotation === 'rotate180') return 180 + return 0 } /** 与 renderLabelPreviewCanvas lineHeightForTextElement 一致 */ diff --git a/美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/LabelCanvas.tsx b/美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/LabelCanvas.tsx index 88e47ef..6a79c81 100644 --- a/美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/LabelCanvas.tsx +++ b/美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/LabelCanvas.tsx @@ -52,6 +52,7 @@ import { textAlignToFlexAlign, verticalAlignToFlexJustify, readElementRotation, + elementRotationDegrees, normalizeElementRotationBox, canonicalElementGeometry, readFontWeight, @@ -1146,6 +1147,8 @@ function formatDateByPreset(format: string, date: Date): string { 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 hour12 = date.getHours() % 12 || 12; + const ampm = date.getHours() >= 12 ? 'pm' : 'am'; 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(); @@ -1179,6 +1182,11 @@ function formatDateByPreset(format: string, date: Date): string { return yyyy; case 'DD MONTH YEAR (25 DECEMBER 2025)': return `${dd} ${monthLong} ${yyyy}`; + case '12 hr': + return `${hour12}:${min}${ampm}`; + case '24 hr': + case 'HH:mm': + return `${hh}:${min}`; default: return format .replace('YYYY', yyyy) @@ -1216,14 +1224,17 @@ function VerticalRotationFrame({ className, boxWidth, boxHeight, + rotationDegrees, }: { children: React.ReactNode; className?: string; boxWidth: number; boxHeight: number; + rotationDegrees: 0 | -90 | 90 | 180; }) { - const innerW = Math.max(1, boxHeight); - const innerH = Math.max(1, boxWidth); + const isQuarterTurn = rotationDegrees === -90 || rotationDegrees === 90; + const innerW = Math.max(1, isQuarterTurn ? boxHeight : boxWidth); + const innerH = Math.max(1, isQuarterTurn ? boxWidth : boxHeight); return (
@@ -1334,7 +1345,8 @@ function ElementContent({ }) { const cfg = el.config as Record; const type = canonicalElementType(el.type); - const isVerticalRotation = readElementRotation(el) === 'vertical'; + const rotationDegrees = elementRotationDegrees(el); + const isVerticalRotation = rotationDegrees !== 0; const wrapVertical = (node: React.ReactNode, className?: string) => isVerticalRotation ? ( @@ -1342,6 +1354,7 @@ function ElementContent({ className={className} boxWidth={el.width} boxHeight={el.height} + rotationDegrees={rotationDegrees} > {node} @@ -1693,7 +1706,9 @@ function ElementContent({ ); } const d = new Date(); - const example = formatDateByPreset('HH:mm', d); + const timeFormatRaw = String(cfg?.format ?? cfg?.Format ?? '24 hr').trim(); + const timeFormat = timeFormatRaw === '12 hr' ? '12 hr' : '24 hr'; + const example = formatDateByPreset(timeFormat, d); return wrapVertical( {example} @@ -1782,9 +1797,9 @@ function ElementContent({ style={ isVerticalRotation ? { - width: el.height, - height: el.width, - transform: 'rotate(-90deg)', + width: rotationDegrees === -90 || rotationDegrees === 90 ? el.height : el.width, + height: rotationDegrees === -90 || rotationDegrees === 90 ? el.width : el.height, + transform: `rotate(${rotationDegrees}deg)`, transformOrigin: 'center center', } : { width: '100%', height: '100%' } @@ -3117,7 +3132,7 @@ export function LabelCanvas({ const isPrintField = isPrintInputElement(el); const isSelected = selectedId === el.id; const positionLocked = readElementPositionLocked(el); - const isVerticalRotation = readElementRotation(effectiveEl) === 'vertical'; + const isVerticalRotation = elementRotationDegrees(effectiveEl) !== 0; return (
- - + +
+ + + + {elementRotationDegrees(selectedElement)}° + +
) : null} {!isBlankElement ? ( @@ -1194,11 +1221,20 @@ function ElementConfigFields({ ); } case 'TIME': + const timeFormat = cfgPickStr(cfg, ['format', 'Format'], '24 hr'); return ( <>
- - + +
diff --git a/美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/index.tsx b/美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/index.tsx index 8dd2cb8..bf7e6af 100644 --- a/美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/index.tsx +++ b/美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/index.tsx @@ -438,7 +438,8 @@ export function LabelTemplateEditor({ patch.rotation !== undefined; if (!geomTouched) return merged; // 竖排且宽>高:先纠正宽高再 clamp,避免 x 被钳到最左边 - if (readElementRotation(merged) === 'vertical' && merged.width > merged.height) { + const mergedRotation = readElementRotation(merged); + if ((mergedRotation === 'vertical' || mergedRotation === 'right') && merged.width > merged.height) { merged = canonicalElementGeometry(merged); } const c = clampLabelElementBox( diff --git a/美国版/Food Labeling Management Platform/src/components/labels/LabelsList.tsx b/美国版/Food Labeling Management Platform/src/components/labels/LabelsList.tsx index 1c28634..e0f5142 100755 --- a/美国版/Food Labeling Management Platform/src/components/labels/LabelsList.tsx +++ b/美国版/Food Labeling Management Platform/src/components/labels/LabelsList.tsx @@ -42,8 +42,8 @@ import { PaginationNext, PaginationPrevious, } from "../ui/pagination"; -import { getLabels, getLabel, createLabel, updateLabel, deleteLabel } from "../../services/labelService"; -import type { LabelDto, LabelCreateInput, LabelUpdateInput } from "../../types/label"; +import { getLabels, getLabel, createLabel, batchCreateLabels, updateLabel, deleteLabel } from "../../services/labelService"; +import type { LabelDto, LabelCreateInput, LabelBatchCreateItemInput, LabelUpdateInput } from "../../types/label"; import { getLocations } from "../../services/locationService"; import { getGroups } from "../../services/groupService"; import { getPartners } from "../../services/partnerService"; @@ -2031,7 +2031,7 @@ function LabelBulkAddPage({ const removeRow = (rowId: string) => setRows((prev) => (prev.length <= 1 ? prev : prev.filter((row) => row.id !== rowId))); - const validateAndBuildInput = (row: BulkAddRow, index: number): LabelCreateInput | null => { + const validateAndBuildInput = (row: BulkAddRow, index: number): LabelBatchCreateItemInput | null => { const rowName = `Row ${index + 1}`; const effectivePartnerId = effectiveScopePartnerId( scopeAuth.requireCompanySelection, @@ -2092,18 +2092,42 @@ function LabelBulkAddPage({ }); return null; } + const productId = row.productId.trim(); + const labelTypeId = row.labelTypeId.trim(); + const product = products.find((x) => x.id === productId); + const labelType = types.find((x) => x.id === labelTypeId); + const productCodeValue = (product?.codeValue ?? "").trim(); + const templateDefaultValues = selectedTemplate + ? collectTemplateDefaultValuesForSave( + selectedTemplate, + row.templateDataValues, + row.templateDateOffsets, + row.nutritionByElementId, + productCodeValue, + { + labelName: row.labelName, + labelTypeName: labelType?.typeName ?? labelType?.typeCode, + }, + ) + : {}; return { labelName: row.labelName.trim(), - templateCode, locationIds: locPayload.locationIds, + locationId: locPayload.locationIds[0] ?? null, labelCategoryId: row.labelCategoryId.trim(), - labelTypeId: row.labelTypeId.trim(), - productIds: [row.productId.trim()], + labelTypeId, + productIds: [productId], labelInfoJson: null, state: row.state, appliedRegionType: regionPayload.appliedRegionType, regionIds: regionPayload.regionIds, groupIds: regionPayload.regionIds, + partnerId: effectivePartnerId, + partnerIds: effectivePartnerId ? [effectivePartnerId] : [], + templateDefaultValues, + templateDataValues: { ...row.templateDataValues }, + templateDateOffsets: { ...row.templateDateOffsets }, + nutritionByElementId: { ...row.nutritionByElementId }, }; }; @@ -2168,31 +2192,46 @@ function LabelBulkAddPage({ }; const submit = async () => { - const inputs: LabelCreateInput[] = []; - const validRows: BulkAddRow[] = []; + const inputs: LabelBatchCreateItemInput[] = []; + if (rows.length > 500) { + toast.error("Too many rows.", { + description: "Bulk Add supports up to 500 rows at a time.", + }); + return; + } for (let i = 0; i < rows.length; i += 1) { const input = validateAndBuildInput(rows[i], i); if (!input) return; inputs.push(input); - validRows.push(rows[i]); } setSaving(true); try { - for (const input of inputs) { - await createLabel(input); - } - try { - await saveTemplateDefaultsAfterLabels(validRows); - } catch (e: any) { - toast.warning("Labels created, template data failed.", { - description: e?.message ? String(e.message) : "Please edit template data manually.", + const result = await batchCreateLabels({ + templateCode: templateCode.trim(), + saveTemplateProductDefaults: true, + items: inputs, + }); + const successCount = Number(result.successCount ?? 0); + const failCount = Number(result.failCount ?? 0); + if (failCount > 0) { + const firstErrors = (result.errors ?? []) + .slice(0, 3) + .map((err) => { + const rowNo = err.rowNumber ? `Row ${err.rowNumber}` : "Row"; + return `${rowNo}: ${err.message || "Failed"}`; + }) + .join("; "); + toast.warning("Bulk Add completed with errors.", { + description: `${successCount} created, ${failCount} failed.${firstErrors ? ` ${firstErrors}` : ""}`, + }); + if (successCount > 0) onBack(); + } else { + toast.success("Labels created.", { + description: `${successCount || inputs.length} label(s) have been created successfully.`, }); + onBack(); } - toast.success("Labels created.", { - description: `${inputs.length} label(s) have been created successfully.`, - }); - onBack(); } catch (e: any) { toast.error("Failed to create labels.", { description: e?.message ? String(e.message) : "Please check the rows and try again.", diff --git a/美国版/Food Labeling Management Platform/src/lib/labelFormDatePreview.ts b/美国版/Food Labeling Management Platform/src/lib/labelFormDatePreview.ts index 012da9a..8837a2a 100644 --- a/美国版/Food Labeling Management Platform/src/lib/labelFormDatePreview.ts +++ b/美国版/Food Labeling Management Platform/src/lib/labelFormDatePreview.ts @@ -58,6 +58,8 @@ export function formatDateByPreset(format: string, date: Date): string { 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 hour12 = date.getHours() % 12 || 12; + const ampm = date.getHours() >= 12 ? "pm" : "am"; 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(); @@ -91,6 +93,11 @@ export function formatDateByPreset(format: string, date: Date): string { return yyyy; case "DD MONTH YEAR (25 DECEMBER 2025)": return `${dd} ${monthLong} ${yyyy}`; + case "12 hr": + return `${hour12}:${min}${ampm}`; + case "24 hr": + case "HH:mm": + return `${hh}:${min}`; default: return format .replace(/YYYY/g, yyyy) @@ -112,7 +119,7 @@ export function isLikelyResolvedDateTimeLiteral(raw: string | null | undefined): 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; + if (/^\d{1,2}:\d{2}(:\d{2})?(\s?[ap]m)?$/i.test(t)) return true; return false; } @@ -206,7 +213,7 @@ export function resolveStoredPrintValueToDisplayText( : format; return formatDateByPreset(fmt, d); } - if (type === "TIME") return formatDateByPreset("HH:mm", d); + if (type === "TIME") return formatDateByPreset(dateFormatPatternForElement(el, cfg), d); } return s; } @@ -230,7 +237,7 @@ export function resolveStoredPrintValueToDisplayText( return formatDateByPreset(format, d); } if (type === "TIME") { - return formatDateByPreset("HH:mm", d); + return formatDateByPreset(dateFormatPatternForElement(el, cfg), d); } return `${amount} ${unit}`; } @@ -244,7 +251,10 @@ function applyElementPrefix(cfg: Record, body: string): string function dateFormatPatternForElement(el: LabelElement, cfg: Record): string { const type = canonicalElementType(el.type); - if (type === "TIME") return "HH:mm"; + if (type === "TIME") { + const raw = String(cfg.format ?? cfg.Format ?? "24 hr").trim(); + return raw === "12 hr" ? "12 hr" : "24 hr"; + } const it = String(cfg.inputType ?? cfg.InputType ?? "").toLowerCase(); const raw = (typeof cfg.format === "string" && cfg.format.trim() @@ -303,7 +313,7 @@ function resolveFromOffsetAmount( return applyElementPrefix(cfg, formatDateByPreset(dateFormatPatternForElement(el, cfg), d)); } if (type === "TIME") { - return applyElementPrefix(cfg, formatDateByPreset("HH:mm", d)); + return applyElementPrefix(cfg, formatDateByPreset(dateFormatPatternForElement(el, cfg), d)); } return applyElementPrefix(cfg, `${amount} ${unit}`.trim()); } @@ -415,4 +425,3 @@ export function applyLiveDateTimePreviewToElements( return { ...el, config: cfg }; }); } - diff --git a/美国版/Food Labeling Management Platform/src/services/labelService.ts b/美国版/Food Labeling Management Platform/src/services/labelService.ts index bea0229..7357318 100644 --- a/美国版/Food Labeling Management Platform/src/services/labelService.ts +++ b/美国版/Food Labeling Management Platform/src/services/labelService.ts @@ -1,6 +1,8 @@ import { createApiClient } from "../lib/apiClient"; import type { LabelCreateInput, + LabelBatchCreateInput, + LabelBatchCreateResult, LabelDto, LabelGetListInput, LabelUpdateInput, @@ -156,6 +158,92 @@ export async function createLabel(input: LabelCreateInput): Promise { return normalizeLabelDto(raw); } +function normalizeBatchCreateResult(raw: unknown): LabelBatchCreateResult { + const r = (raw && typeof raw === "object" ? raw : {}) as Record; + const successItemsRaw = r.successItems ?? r.SuccessItems; + const errorsRaw = r.errors ?? r.Errors; + const successItems = Array.isArray(successItemsRaw) + ? successItemsRaw.map((x) => { + const it = (x && typeof x === "object" ? x : {}) as Record; + return { + rowNumber: typeof (it.rowNumber ?? it.RowNumber) === "number" ? (it.rowNumber ?? it.RowNumber) as number : null, + labelCode: it.labelCode != null || it.LabelCode != null ? String(it.labelCode ?? it.LabelCode) : null, + labelName: it.labelName != null || it.LabelName != null ? String(it.labelName ?? it.LabelName) : null, + }; + }) + : []; + const errors = Array.isArray(errorsRaw) + ? errorsRaw.map((x) => { + const it = (x && typeof x === "object" ? x : {}) as Record; + return { + rowNumber: typeof (it.rowNumber ?? it.RowNumber) === "number" ? (it.rowNumber ?? it.RowNumber) as number : null, + labelName: it.labelName != null || it.LabelName != null ? String(it.labelName ?? it.LabelName) : null, + message: it.message != null || it.Message != null ? String(it.message ?? it.Message) : null, + }; + }) + : []; + return { + successCount: Number(r.successCount ?? r.SuccessCount ?? successItems.length) || 0, + failCount: Number(r.failCount ?? r.FailCount ?? errors.length) || 0, + successItems, + errors, + }; +} + +export async function batchCreateLabels(input: LabelBatchCreateInput): Promise { + const raw = await api.requestJson({ + path: `${PATH}/batch-create`, + method: "POST", + body: { + templateCode: input.templateCode, + saveTemplateProductDefaults: input.saveTemplateProductDefaults ?? true, + items: input.items.map((item) => { + const locationIds = [ + ...new Set((item.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean)), + ]; + const regionIds = [ + ...new Set( + [...(item.regionIds ?? []), ...(item.groupIds ?? [])] + .map((x) => String(x).trim()) + .filter(Boolean), + ), + ]; + const partnerIds = [ + ...new Set( + [ + ...(item.partnerId ? [item.partnerId] : []), + ...(item.partnerIds ?? []), + ] + .map((x) => String(x).trim()) + .filter(Boolean), + ), + ]; + return { + labelCode: String(item.labelCode ?? "").trim() || null, + labelName: item.labelName, + labelCategoryId: item.labelCategoryId, + labelTypeId: String(item.labelTypeId ?? "").trim() || null, + productIds: item.productIds, + locationIds, + locationId: item.locationId ?? locationIds[0] ?? null, + appliedRegionType: item.appliedRegionType, + regionIds, + groupIds: regionIds, + partnerId: item.partnerId ?? partnerIds[0] ?? null, + partnerIds, + labelInfoJson: item.labelInfoJson ?? null, + state: item.state ?? true, + templateDefaultValues: item.templateDefaultValues ?? {}, + templateDataValues: item.templateDataValues ?? {}, + templateDateOffsets: item.templateDateOffsets ?? {}, + nutritionByElementId: item.nutritionByElementId ?? {}, + }; + }), + }, + }); + return normalizeBatchCreateResult(raw); +} + export async function updateLabel(labelCode: string, input: LabelUpdateInput): Promise { const raw = await api.requestJson({ path: `${PATH}/${encodeURIComponent(labelCode)}`, diff --git a/美国版/Food Labeling Management Platform/src/types/label.ts b/美国版/Food Labeling Management Platform/src/types/label.ts index ee64a7f..2ddd7c9 100644 --- a/美国版/Food Labeling Management Platform/src/types/label.ts +++ b/美国版/Food Labeling Management Platform/src/types/label.ts @@ -75,6 +75,57 @@ export type LabelCreateInput = { groupIds?: string[] | null; }; +export type LabelBatchDateOffsetInput = { + unit: string; + value: string; +}; + +export type LabelBatchCreateItemInput = { + labelCode?: string | null; + labelName: string; + labelCategoryId: string; + labelTypeId?: string | null; + productIds: string[]; + locationIds?: string[] | null; + locationId?: string | null; + appliedRegionType?: "ALL" | "SPECIFIED" | string | null; + regionIds?: string[] | null; + groupIds?: string[] | null; + partnerId?: string | null; + partnerIds?: string[] | null; + labelInfoJson?: Record | null; + state?: boolean; + templateDefaultValues?: Record; + templateDataValues?: Record; + templateDateOffsets?: Record; + nutritionByElementId?: Record>; +}; + +export type LabelBatchCreateInput = { + templateCode: string; + saveTemplateProductDefaults?: boolean; + items: LabelBatchCreateItemInput[]; +}; + +export type LabelBatchCreateSuccessItem = { + rowNumber?: number | null; + labelCode?: string | null; + labelName?: string | null; +}; + +export type LabelBatchCreateErrorItem = { + rowNumber?: number | null; + labelName?: string | null; + message?: string | null; +}; + +export type LabelBatchCreateResult = { + successCount: number; + failCount: number; + successItems: LabelBatchCreateSuccessItem[]; + errors: LabelBatchCreateErrorItem[]; +}; + export type LabelUpdateInput = { labelName: string; templateCode: string; diff --git a/美国版/Food Labeling Management Platform/src/types/labelTemplate.ts b/美国版/Food Labeling Management Platform/src/types/labelTemplate.ts index 74efde3..20c5861 100644 --- a/美国版/Food Labeling Management Platform/src/types/labelTemplate.ts +++ b/美国版/Food Labeling Management Platform/src/types/labelTemplate.ts @@ -8,7 +8,7 @@ export type LabelType = 'PRICE' | 'NUTRITION' | 'SHIPPING'; export type Unit = 'cm' | 'inch'; /** 与接口文档一致:ALL=全部门店,SPECIFIED=指定门店(需 appliedLocationIds) */ export type AppliedLocation = 'ALL' | 'SPECIFIED'; -export type Rotation = 'horizontal' | 'vertical'; +export type Rotation = 'horizontal' | 'vertical' | 'left' | 'right' | 'rotate180'; /** 模板打印方向:vertical=竖打(默认,内容不旋转);horizontal=横打(纸张 W×H 不变,内容整体旋转 90°) */ export type PrintOrientation = 'horizontal' | 'vertical'; @@ -659,7 +659,7 @@ export function createDefaultElement(type: ElementType, x = 20, y = 20): LabelEl height: 24, config: { format: 'DD/MM/YYYY', offsetDays: 0, fontFamily: editorFont, fontSize: 14, textAlign: 'left' }, }, - TIME: { width: 100, height: 24, config: { format: 'HH:mm', offsetDays: 0, fontFamily: editorFont, fontSize: 14, textAlign: 'left' } }, + TIME: { width: 100, height: 24, config: { format: '24 hr', offsetDays: 0, fontFamily: editorFont, fontSize: 14, textAlign: 'left' } }, DURATION: { width: 120, height: 24, diff --git a/美国版/Food Labeling Management Platform/src/utils/textElementLayout.ts b/美国版/Food Labeling Management Platform/src/utils/textElementLayout.ts index 06648a8..859483b 100644 --- a/美国版/Food Labeling Management Platform/src/utils/textElementLayout.ts +++ b/美国版/Food Labeling Management Platform/src/utils/textElementLayout.ts @@ -75,9 +75,44 @@ export function readElementBorder( /** 读取元素 rotation(兼容大小写 / 旧数据) */ export function readElementRotation( el: { rotation?: string | null }, -): 'horizontal' | 'vertical' { +): 'horizontal' | 'vertical' | 'right' | 'rotate180' { const v = String(el.rotation ?? 'horizontal').trim().toLowerCase(); - return v === 'vertical' ? 'vertical' : 'horizontal'; + if (v === 'vertical' || v === 'left' || v === 'rotate-left' || v === 'left90') return 'vertical'; + if (v === 'right' || v === 'rotate-right' || v === 'right90') return 'right'; + if (v === 'rotate180' || v === '180' || v === 'down' || v === 'inverted') return 'rotate180'; + return 'horizontal'; +} + +export function elementRotationDegrees( + el: { rotation?: string | null }, +): 0 | -90 | 90 | 180 { + const rot = readElementRotation(el); + if (rot === 'vertical') return -90; + if (rot === 'right') return 90; + if (rot === 'rotate180') return 180; + return 0; +} + +export function rotateElementLeftValue( + el: { rotation?: string | null }, +): 'horizontal' | 'vertical' | 'right' | 'rotate180' { + const current = elementRotationDegrees(el); + const next = (((current - 90) % 360) + 360) % 360; + if (next === 270) return 'vertical'; + if (next === 180) return 'rotate180'; + if (next === 90) return 'right'; + return 'horizontal'; +} + +export function rotateElementRightValue( + el: { rotation?: string | null }, +): 'horizontal' | 'vertical' | 'right' | 'rotate180' { + const current = elementRotationDegrees(el); + const next = (((current + 90) % 360) + 360) % 360; + if (next === 270) return 'vertical'; + if (next === 180) return 'rotate180'; + if (next === 90) return 'right'; + return 'horizontal'; } /** 与 App lineHeightForTextElement 一致:leading-tight ≈ 1.25 */ @@ -124,18 +159,19 @@ export function fitFontSizeToInnerWidthEstimate( /** 切换 horizontal / vertical 时交换宽高并保持中心点不变(与参考图 2 一致) */ export function patchElementRotationWithLayout( el: { x: number; y: number; width: number; height: number; rotation?: string | null }, - nextRotation: 'horizontal' | 'vertical', + nextRotation: 'horizontal' | 'vertical' | 'right' | 'rotate180', ): { - rotation: 'horizontal' | 'vertical'; + rotation: 'horizontal' | 'vertical' | 'right' | 'rotate180'; x: number; y: number; width: number; height: number; } { const prev = readElementRotation(el); + const shouldSwapBox = (rotation: string) => rotation === 'vertical' || rotation === 'right'; if (prev === nextRotation) { // vertical 但宽>高:选框与文字方向不一致,强制纠正 - if (nextRotation === 'vertical' && el.width > el.height) { + if (shouldSwapBox(nextRotation) && el.width > el.height) { const w = Math.max(1, el.width); const h = Math.max(1, el.height); const cx = el.x + w / 2; @@ -158,6 +194,15 @@ export function patchElementRotationWithLayout( height: el.height, }; } + if (!shouldSwapBox(prev) && !shouldSwapBox(nextRotation)) { + return { + rotation: nextRotation, + x: el.x, + y: el.y, + width: el.width, + height: el.height, + }; + } const w = Math.max(1, el.width); const h = Math.max(1, el.height); const cx = el.x + w / 2; @@ -184,11 +229,12 @@ export function canonicalElementGeometry< export function normalizeElementRotationBox< T extends { rotation?: string | null; width: number; height: number; x: number; y: number }, >(el: T): T { - if (readElementRotation(el) !== 'vertical') return el; + const rotation = readElementRotation(el); + if (rotation !== 'vertical' && rotation !== 'right') return el; if (el.width <= el.height) return el; return { ...el, - rotation: 'vertical', - ...patchElementRotationWithLayout({ ...el, rotation: 'horizontal' }, 'vertical'), + rotation, + ...patchElementRotationWithLayout({ ...el, rotation: 'horizontal' }, rotation), }; } -- libgit2 0.21.4