Commit 40929347cadf2dac8f0d1dfc7e05f674bfb26440

Authored by 杨鑫
1 parent 2a0e4d1d

feat: update label bulk add and template rotation

美国版/Food Labeling Management App UniApp/src/utils/labelPreview/normalizePreviewTemplate.ts
@@ -691,7 +691,7 @@ export function normalizeLabelTemplateFromPreviewApi(payload: unknown): SystemLa @@ -691,7 +691,7 @@ export function normalizeLabelTemplateFromPreviewApi(payload: unknown): SystemLa
691 y: Number(e.y ?? e.posY ?? e.PosY ?? 0), 691 y: Number(e.y ?? e.posY ?? e.PosY ?? 0),
692 width: Number(e.width ?? e.Width ?? 0), 692 width: Number(e.width ?? e.Width ?? 0),
693 height: Number(e.height ?? e.Height ?? 0), 693 height: Number(e.height ?? e.Height ?? 0),
694 - rotation: String(e.rotation ?? e.Rotation ?? 'horizontal') as 'horizontal' | 'vertical', 694 + rotation: String(e.rotation ?? e.Rotation ?? 'horizontal') as 'horizontal' | 'vertical' | 'left' | 'right' | 'rotate180',
695 border: String(e.border ?? e.Border ?? e.BorderType ?? e.borderType ?? 'none'), 695 border: String(e.border ?? e.Border ?? e.BorderType ?? e.borderType ?? 'none'),
696 config: cfg as Record<string, any>, 696 config: cfg as Record<string, any>,
697 zIndex: Number(e.zIndex ?? e.ZIndex ?? 0), 697 zIndex: Number(e.zIndex ?? e.ZIndex ?? 0),
美国版/Food Labeling Management App UniApp/src/utils/labelPreview/printInputOffset.ts
@@ -32,6 +32,8 @@ export const DATE_DISPLAY_FORMAT_PRESETS = new Set([ @@ -32,6 +32,8 @@ export const DATE_DISPLAY_FORMAT_PRESETS = new Set([
32 'YYYY-MM-DD', 32 'YYYY-MM-DD',
33 'YYYY-MM-DD HH:mm', 33 'YYYY-MM-DD HH:mm',
34 'HH:mm', 34 'HH:mm',
  35 + '12 hr',
  36 + '24 hr',
35 ]) 37 ])
36 38
37 export function isKnownDateDisplayFormat (raw: string | null | undefined): boolean { 39 export function isKnownDateDisplayFormat (raw: string | null | undefined): boolean {
@@ -372,6 +374,8 @@ function formatDateByPreset (format: string, date: Date): string { @@ -372,6 +374,8 @@ function formatDateByPreset (format: string, date: Date): string {
372 const dd = String(date.getDate()).padStart(2, '0') 374 const dd = String(date.getDate()).padStart(2, '0')
373 const hh = String(date.getHours()).padStart(2, '0') 375 const hh = String(date.getHours()).padStart(2, '0')
374 const min = String(date.getMinutes()).padStart(2, '0') 376 const min = String(date.getMinutes()).padStart(2, '0')
  377 + const hour12 = date.getHours() % 12 || 12
  378 + const ampm = date.getHours() >= 12 ? 'pm' : 'am'
375 const monthLong = monthLongEn(date) 379 const monthLong = monthLongEn(date)
376 const dayLong = weekdayLongEn(date) 380 const dayLong = weekdayLongEn(date)
377 const dayShort = weekdayShortEn(date) 381 const dayShort = weekdayShortEn(date)
@@ -408,6 +412,9 @@ function formatDateByPreset (format: string, date: Date): string { @@ -408,6 +412,9 @@ function formatDateByPreset (format: string, date: Date): string {
408 return `${yyyy}-${mm}-${dd}` 412 return `${yyyy}-${mm}-${dd}`
409 case 'YYYY-MM-DD HH:mm': 413 case 'YYYY-MM-DD HH:mm':
410 return `${yyyy}-${mm}-${dd} ${hh}:${min}` 414 return `${yyyy}-${mm}-${dd} ${hh}:${min}`
  415 + case '12 hr':
  416 + return `${hour12}:${min}${ampm}`
  417 + case '24 hr':
411 case 'HH:mm': 418 case 'HH:mm':
412 return `${hh}:${min}` 419 return `${hh}:${min}`
413 default: 420 default:
@@ -432,7 +439,7 @@ export function isLikelyResolvedDateTimeLiteral (raw: string | null | undefined) @@ -432,7 +439,7 @@ export function isLikelyResolvedDateTimeLiteral (raw: string | null | undefined)
432 if (/^\d{4}-\d{2}-\d{2}/.test(t)) return true 439 if (/^\d{4}-\d{2}-\d{2}/.test(t)) return true
433 if (/^\d{4}-\d{2}-\d{2}\s+\d{1,2}:\d{2}/.test(t)) return true 440 if (/^\d{4}-\d{2}-\d{2}\s+\d{1,2}:\d{2}/.test(t)) return true
434 if (/^\d{1,2}\/\d{1,2}\/\d{2,4}/.test(t)) return true 441 if (/^\d{1,2}\/\d{1,2}\/\d{2,4}/.test(t)) return true
435 - if (/^\d{1,2}:\d{2}(:\d{2})?$/.test(t)) return true 442 + if (/^\d{1,2}:\d{2}(:\d{2})?(\s?[ap]m)?$/i.test(t)) return true
436 return false 443 return false
437 } 444 }
438 445
@@ -462,7 +469,10 @@ function dateFormatPatternForElement ( @@ -462,7 +469,10 @@ function dateFormatPatternForElement (
462 cfg: Record<string, unknown>, 469 cfg: Record<string, unknown>,
463 ): string { 470 ): string {
464 const type = String(el.type || '').toUpperCase() 471 const type = String(el.type || '').toUpperCase()
465 - if (type === 'TIME') return 'HH:mm' 472 + if (type === 'TIME') {
  473 + const raw = String(cfg.format ?? cfg.Format ?? '24 hr').trim()
  474 + return raw === '12 hr' ? '12 hr' : '24 hr'
  475 + }
466 476
467 /** 仅底部黑条星期走 FULLY DAY;Use By 仍用 MM/DD/YYYY */ 477 /** 仅底部黑条星期走 FULLY DAY;Use By 仍用 MM/DD/YYYY */
468 if (isWeekdayBarElement({ ...el, config: cfg })) { 478 if (isWeekdayBarElement({ ...el, config: cfg })) {
@@ -561,7 +571,7 @@ function resolveFromOffsetAmount ( @@ -561,7 +571,7 @@ function resolveFromOffsetAmount (
561 return applyElementPrefix(cfg, formatDateByPreset(dateFormatPatternForElement(el, cfg), d)) 571 return applyElementPrefix(cfg, formatDateByPreset(dateFormatPatternForElement(el, cfg), d))
562 } 572 }
563 if (type === 'TIME') { 573 if (type === 'TIME') {
564 - return applyElementPrefix(cfg, formatDateByPreset('HH:mm', d)) 574 + return applyElementPrefix(cfg, formatDateByPreset(dateFormatPatternForElement(el, cfg), d))
565 } 575 }
566 return applyElementPrefix(cfg, `${amount} ${unit}`.trim()) 576 return applyElementPrefix(cfg, `${amount} ${unit}`.trim())
567 } 577 }
美国版/Food Labeling Management App UniApp/src/utils/labelPreview/renderLabelPreviewCanvas.ts
@@ -30,6 +30,7 @@ import { @@ -30,6 +30,7 @@ import {
30 readFontWeight, 30 readFontWeight,
31 readTextDecoration, 31 readTextDecoration,
32 readElementRotation, 32 readElementRotation,
  33 + elementRotationDegrees,
33 isShortSingleLineTextBox, 34 isShortSingleLineTextBox,
34 type TextVerticalAlign, 35 type TextVerticalAlign,
35 } from '../textElementLayout' 36 } from '../textElementLayout'
@@ -1233,7 +1234,7 @@ function runLabelPreviewCanvasDraw( @@ -1233,7 +1234,7 @@ function runLabelPreviewCanvasDraw(
1233 if (!finalText && weekdayBar) { 1234 if (!finalText && weekdayBar) {
1234 finalText = resolveWeekdayDisplayForElement(el, baseTime) 1235 finalText = resolveWeekdayDisplayForElement(el, baseTime)
1235 } 1236 }
1236 - const rotation = String(el.rotation ?? (el as any).Rotation ?? 'horizontal').toLowerCase() 1237 + const rotationDegrees = elementRotationDegrees(el as any)
1237 const drawAt = (bx: number, by: number, bw: number, bh: number) => { 1238 const drawAt = (bx: number, by: number, bw: number, bh: number) => {
1238 const anyCtx = ctx as any 1239 const anyCtx = ctx as any
1239 if (typeof anyCtx.save === 'function') anyCtx.save() 1240 if (typeof anyCtx.save === 'function') anyCtx.save()
@@ -1353,13 +1354,19 @@ function runLabelPreviewCanvasDraw( @@ -1353,13 +1354,19 @@ function runLabelPreviewCanvasDraw(
1353 if (typeof anyCtx.restore === 'function') anyCtx.restore() 1354 if (typeof anyCtx.restore === 'function') anyCtx.restore()
1354 } 1355 }
1355 1356
1356 - if (rotation === 'vertical') { 1357 + if (rotationDegrees !== 0) {
1357 const anyCtx = ctx as any 1358 const anyCtx = ctx as any
1358 if (typeof anyCtx.save === 'function' && typeof anyCtx.rotate === 'function') { 1359 if (typeof anyCtx.save === 'function' && typeof anyCtx.rotate === 'function') {
  1360 + const isQuarterTurn = rotationDegrees === -90 || rotationDegrees === 90
1359 anyCtx.save() 1361 anyCtx.save()
1360 anyCtx.translate(x + w / 2, y + h / 2) 1362 anyCtx.translate(x + w / 2, y + h / 2)
1361 - anyCtx.rotate(-Math.PI / 2)  
1362 - drawAt(-h / 2, -w / 2, h, w) 1363 + anyCtx.rotate((rotationDegrees * Math.PI) / 180)
  1364 + drawAt(
  1365 + isQuarterTurn ? -h / 2 : -w / 2,
  1366 + isQuarterTurn ? -w / 2 : -h / 2,
  1367 + isQuarterTurn ? h : w,
  1368 + isQuarterTurn ? w : h,
  1369 + )
1363 anyCtx.restore() 1370 anyCtx.restore()
1364 } else { 1371 } else {
1365 drawAt(x, y, w, h) 1372 drawAt(x, y, w, h)
美国版/Food Labeling Management App UniApp/src/utils/print/nativeTemplateElementSupport.ts
@@ -244,7 +244,7 @@ export function templateRequiresCanvasStyleFidelity (template: SystemLabelTempla @@ -244,7 +244,7 @@ export function templateRequiresCanvasStyleFidelity (template: SystemLabelTempla
244 if (border === 'line' || border === 'dotted' || border === 'solid') { 244 if (border === 'line' || border === 'dotted' || border === 'solid') {
245 return true 245 return true
246 } 246 }
247 - if (readElementRotation(el) === 'vertical') { 247 + if (readElementRotation(el) !== 'horizontal') {
248 return true 248 return true
249 } 249 }
250 const cfg = (el.config || {}) as Record<string, unknown> 250 const cfg = (el.config || {}) as Record<string, unknown>
美国版/Food Labeling Management App UniApp/src/utils/print/systemTemplateAdapter.ts
@@ -261,7 +261,11 @@ function toEscAlign (align: SystemTemplateTextAlign): 0 | 1 | 2 { @@ -261,7 +261,11 @@ function toEscAlign (align: SystemTemplateTextAlign): 0 | 1 | 2 {
261 } 261 }
262 262
263 function resolveRotation (value?: string): number { 263 function resolveRotation (value?: string): number {
264 - return value === 'vertical' ? 90 : 0 264 + const rotation = String(value || 'horizontal').trim().toLowerCase()
  265 + if (rotation === 'vertical' || rotation === 'left' || rotation === 'rotate-left' || rotation === 'left90') return 90
  266 + if (rotation === 'right' || rotation === 'rotate-right' || rotation === 'right90') return 270
  267 + if (rotation === 'rotate180' || rotation === '180' || rotation === 'down' || rotation === 'inverted') return 180
  268 + return 0
265 } 269 }
266 270
267 function normalizeQrLevel (value?: string): 'L' | 'M' | 'Q' | 'H' { 271 function normalizeQrLevel (value?: string): 'L' | 'M' | 'Q' | 'H' {
美国版/Food Labeling Management App UniApp/src/utils/print/types/printer.ts
@@ -68,7 +68,7 @@ export interface RawImageDataSource { @@ -68,7 +68,7 @@ export interface RawImageDataSource {
68 export type LabelTemplateValue = string | number 68 export type LabelTemplateValue = string | number
69 export type LabelTemplateData = Record<string, LabelTemplateValue> 69 export type LabelTemplateData = Record<string, LabelTemplateValue>
70 export type PrinterTemplateUnit = 'inch' | 'mm' | 'cm' | 'px' 70 export type PrinterTemplateUnit = 'inch' | 'mm' | 'cm' | 'px'
71 -export type SystemTemplateRotation = 'horizontal' | 'vertical' 71 +export type SystemTemplateRotation = 'horizontal' | 'vertical' | 'left' | 'right' | 'rotate180'
72 export type SystemTemplatePrintOrientation = 'horizontal' | 'vertical' 72 export type SystemTemplatePrintOrientation = 'horizontal' | 'vertical'
73 export type SystemTemplateTextAlign = 'left' | 'center' | 'right' 73 export type SystemTemplateTextAlign = 'left' | 'center' | 'right'
74 74
美国版/Food Labeling Management App UniApp/src/utils/textElementLayout.ts
@@ -61,9 +61,22 @@ export function readElementBorder( @@ -61,9 +61,22 @@ export function readElementBorder(
61 /** 元素 rotation(兼容大小写) */ 61 /** 元素 rotation(兼容大小写) */
62 export function readElementRotation( 62 export function readElementRotation(
63 el: { rotation?: string | null; Rotation?: string | null }, 63 el: { rotation?: string | null; Rotation?: string | null },
64 -): 'horizontal' | 'vertical' { 64 +): 'horizontal' | 'vertical' | 'right' | 'rotate180' {
65 const v = String(el.rotation ?? el.Rotation ?? 'horizontal').trim().toLowerCase() 65 const v = String(el.rotation ?? el.Rotation ?? 'horizontal').trim().toLowerCase()
66 - return v === 'vertical' ? 'vertical' : 'horizontal' 66 + if (v === 'vertical' || v === 'left' || v === 'rotate-left' || v === 'left90') return 'vertical'
  67 + if (v === 'right' || v === 'rotate-right' || v === 'right90') return 'right'
  68 + if (v === 'rotate180' || v === '180' || v === 'down' || v === 'inverted') return 'rotate180'
  69 + return 'horizontal'
  70 +}
  71 +
  72 +export function elementRotationDegrees(
  73 + el: { rotation?: string | null; Rotation?: string | null },
  74 +): 0 | -90 | 90 | 180 {
  75 + const rotation = readElementRotation(el)
  76 + if (rotation === 'vertical') return -90
  77 + if (rotation === 'right') return 90
  78 + if (rotation === 'rotate180') return 180
  79 + return 0
67 } 80 }
68 81
69 /** 与 renderLabelPreviewCanvas lineHeightForTextElement 一致 */ 82 /** 与 renderLabelPreviewCanvas lineHeightForTextElement 一致 */
美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/LabelCanvas.tsx
@@ -52,6 +52,7 @@ import { @@ -52,6 +52,7 @@ import {
52 textAlignToFlexAlign, 52 textAlignToFlexAlign,
53 verticalAlignToFlexJustify, 53 verticalAlignToFlexJustify,
54 readElementRotation, 54 readElementRotation,
  55 + elementRotationDegrees,
55 normalizeElementRotationBox, 56 normalizeElementRotationBox,
56 canonicalElementGeometry, 57 canonicalElementGeometry,
57 readFontWeight, 58 readFontWeight,
@@ -1146,6 +1147,8 @@ function formatDateByPreset(format: string, date: Date): string { @@ -1146,6 +1147,8 @@ function formatDateByPreset(format: string, date: Date): string {
1146 const dd = String(date.getDate()).padStart(2, '0'); 1147 const dd = String(date.getDate()).padStart(2, '0');
1147 const hh = String(date.getHours()).padStart(2, '0'); 1148 const hh = String(date.getHours()).padStart(2, '0');
1148 const min = String(date.getMinutes()).padStart(2, '0'); 1149 const min = String(date.getMinutes()).padStart(2, '0');
  1150 + const hour12 = date.getHours() % 12 || 12;
  1151 + const ampm = date.getHours() >= 12 ? 'pm' : 'am';
1149 const monthLong = date.toLocaleString('en-US', { month: 'long' }).toUpperCase(); 1152 const monthLong = date.toLocaleString('en-US', { month: 'long' }).toUpperCase();
1150 const dayLong = date.toLocaleString('en-US', { weekday: 'long' }).toUpperCase(); 1153 const dayLong = date.toLocaleString('en-US', { weekday: 'long' }).toUpperCase();
1151 const dayShort = date.toLocaleString('en-US', { weekday: 'short' }).toUpperCase(); 1154 const dayShort = date.toLocaleString('en-US', { weekday: 'short' }).toUpperCase();
@@ -1179,6 +1182,11 @@ function formatDateByPreset(format: string, date: Date): string { @@ -1179,6 +1182,11 @@ function formatDateByPreset(format: string, date: Date): string {
1179 return yyyy; 1182 return yyyy;
1180 case 'DD MONTH YEAR (25 DECEMBER 2025)': 1183 case 'DD MONTH YEAR (25 DECEMBER 2025)':
1181 return `${dd} ${monthLong} ${yyyy}`; 1184 return `${dd} ${monthLong} ${yyyy}`;
  1185 + case '12 hr':
  1186 + return `${hour12}:${min}${ampm}`;
  1187 + case '24 hr':
  1188 + case 'HH:mm':
  1189 + return `${hh}:${min}`;
1182 default: 1190 default:
1183 return format 1191 return format
1184 .replace('YYYY', yyyy) 1192 .replace('YYYY', yyyy)
@@ -1216,14 +1224,17 @@ function VerticalRotationFrame({ @@ -1216,14 +1224,17 @@ function VerticalRotationFrame({
1216 className, 1224 className,
1217 boxWidth, 1225 boxWidth,
1218 boxHeight, 1226 boxHeight,
  1227 + rotationDegrees,
1219 }: { 1228 }: {
1220 children: React.ReactNode; 1229 children: React.ReactNode;
1221 className?: string; 1230 className?: string;
1222 boxWidth: number; 1231 boxWidth: number;
1223 boxHeight: number; 1232 boxHeight: number;
  1233 + rotationDegrees: 0 | -90 | 90 | 180;
1224 }) { 1234 }) {
1225 - const innerW = Math.max(1, boxHeight);  
1226 - const innerH = Math.max(1, boxWidth); 1235 + const isQuarterTurn = rotationDegrees === -90 || rotationDegrees === 90;
  1236 + const innerW = Math.max(1, isQuarterTurn ? boxHeight : boxWidth);
  1237 + const innerH = Math.max(1, isQuarterTurn ? boxWidth : boxHeight);
1227 return ( 1238 return (
1228 <div className={cn('relative flex h-full w-full items-center justify-center overflow-visible', className)}> 1239 <div className={cn('relative flex h-full w-full items-center justify-center overflow-visible', className)}>
1229 <div 1240 <div
@@ -1231,7 +1242,7 @@ function VerticalRotationFrame({ @@ -1231,7 +1242,7 @@ function VerticalRotationFrame({
1231 style={{ 1242 style={{
1232 width: innerW, 1243 width: innerW,
1233 height: innerH, 1244 height: innerH,
1234 - transform: 'rotate(-90deg)', 1245 + transform: `rotate(${rotationDegrees}deg)`,
1235 transformOrigin: 'center center', 1246 transformOrigin: 'center center',
1236 }} 1247 }}
1237 > 1248 >
@@ -1334,7 +1345,8 @@ function ElementContent({ @@ -1334,7 +1345,8 @@ function ElementContent({
1334 }) { 1345 }) {
1335 const cfg = el.config as Record<string, unknown>; 1346 const cfg = el.config as Record<string, unknown>;
1336 const type = canonicalElementType(el.type); 1347 const type = canonicalElementType(el.type);
1337 - const isVerticalRotation = readElementRotation(el) === 'vertical'; 1348 + const rotationDegrees = elementRotationDegrees(el);
  1349 + const isVerticalRotation = rotationDegrees !== 0;
1338 1350
1339 const wrapVertical = (node: React.ReactNode, className?: string) => 1351 const wrapVertical = (node: React.ReactNode, className?: string) =>
1340 isVerticalRotation ? ( 1352 isVerticalRotation ? (
@@ -1342,6 +1354,7 @@ function ElementContent({ @@ -1342,6 +1354,7 @@ function ElementContent({
1342 className={className} 1354 className={className}
1343 boxWidth={el.width} 1355 boxWidth={el.width}
1344 boxHeight={el.height} 1356 boxHeight={el.height}
  1357 + rotationDegrees={rotationDegrees}
1345 > 1358 >
1346 {node} 1359 {node}
1347 </VerticalRotationFrame> 1360 </VerticalRotationFrame>
@@ -1693,7 +1706,9 @@ function ElementContent({ @@ -1693,7 +1706,9 @@ function ElementContent({
1693 ); 1706 );
1694 } 1707 }
1695 const d = new Date(); 1708 const d = new Date();
1696 - const example = formatDateByPreset('HH:mm', d); 1709 + const timeFormatRaw = String(cfg?.format ?? cfg?.Format ?? '24 hr').trim();
  1710 + const timeFormat = timeFormatRaw === '12 hr' ? '12 hr' : '24 hr';
  1711 + const example = formatDateByPreset(timeFormat, d);
1697 return wrapVertical( 1712 return wrapVertical(
1698 <AlignedTextBox cfg={cfg} commonStyle={commonStyle} className="whitespace-nowrap"> 1713 <AlignedTextBox cfg={cfg} commonStyle={commonStyle} className="whitespace-nowrap">
1699 {example} 1714 {example}
@@ -1782,9 +1797,9 @@ function ElementContent({ @@ -1782,9 +1797,9 @@ function ElementContent({
1782 style={ 1797 style={
1783 isVerticalRotation 1798 isVerticalRotation
1784 ? { 1799 ? {
1785 - width: el.height,  
1786 - height: el.width,  
1787 - transform: 'rotate(-90deg)', 1800 + width: rotationDegrees === -90 || rotationDegrees === 90 ? el.height : el.width,
  1801 + height: rotationDegrees === -90 || rotationDegrees === 90 ? el.width : el.height,
  1802 + transform: `rotate(${rotationDegrees}deg)`,
1788 transformOrigin: 'center center', 1803 transformOrigin: 'center center',
1789 } 1804 }
1790 : { width: '100%', height: '100%' } 1805 : { width: '100%', height: '100%' }
@@ -3117,7 +3132,7 @@ export function LabelCanvas({ @@ -3117,7 +3132,7 @@ export function LabelCanvas({
3117 const isPrintField = isPrintInputElement(el); 3132 const isPrintField = isPrintInputElement(el);
3118 const isSelected = selectedId === el.id; 3133 const isSelected = selectedId === el.id;
3119 const positionLocked = readElementPositionLocked(el); 3134 const positionLocked = readElementPositionLocked(el);
3120 - const isVerticalRotation = readElementRotation(effectiveEl) === 'vertical'; 3135 + const isVerticalRotation = elementRotationDegrees(effectiveEl) !== 0;
3121 return ( 3136 return (
3122 <div 3137 <div
3123 key={el.id} 3138 key={el.id}
美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/PropertiesPanel.tsx
1 import React, { useCallback, useEffect, useState } from 'react'; 1 import React, { useCallback, useEffect, useState } from 'react';
2 -import { Building2, Check, Mail, Map, MapPin, Mailbox, Trash2 } from 'lucide-react'; 2 +import { Building2, Check, Mail, Map, MapPin, Mailbox, RotateCcw, RotateCw, Trash2 } from 'lucide-react';
3 import { Input } from '../../ui/input'; 3 import { Input } from '../../ui/input';
4 import { Button } from '../../ui/button'; 4 import { Button } from '../../ui/button';
5 import { Label } from '../../ui/label'; 5 import { Label } from '../../ui/label';
@@ -42,7 +42,15 @@ import { @@ -42,7 +42,15 @@ import {
42 } from '../../../utils/companyPrintFields'; 42 } from '../../../utils/companyPrintFields';
43 import { cn } from '../../ui/utils'; 43 import { cn } from '../../ui/utils';
44 import { readInvertColors, isTextLikeElementForInvertColors } from '../../../utils/invertColorsConfig'; 44 import { readInvertColors, isTextLikeElementForInvertColors } from '../../../utils/invertColorsConfig';
45 -import { readVerticalAlign, readElementRotation, readFontWeight, readFontStyle, readTextDecoration } from '../../../utils/textElementLayout'; 45 +import {
  46 + elementRotationDegrees,
  47 + readVerticalAlign,
  48 + rotateElementLeftValue,
  49 + rotateElementRightValue,
  50 + readFontWeight,
  51 + readFontStyle,
  52 + readTextDecoration,
  53 +} from '../../../utils/textElementLayout';
46 import { 54 import {
47 type PreviewRulerDisplayUnit, 55 type PreviewRulerDisplayUnit,
48 displayLengthToElementPx, 56 displayLengthToElementPx,
@@ -209,23 +217,42 @@ export function PropertiesPanel({ @@ -209,23 +217,42 @@ export function PropertiesPanel({
209 ) : null} 217 ) : null}
210 {!isBlankElement ? ( 218 {!isBlankElement ? (
211 <div> 219 <div>
212 - <Label className="text-xs">Rotation</Label>  
213 - <Select  
214 - value={readElementRotation(selectedElement)}  
215 - onValueChange={(v: Rotation) =>  
216 - onElementChange(selectedElement.id, {  
217 - rotation: v,  
218 - })  
219 - }  
220 - >  
221 - <SelectTrigger className="h-8 text-sm">  
222 - <SelectValue />  
223 - </SelectTrigger>  
224 - <SelectContent>  
225 - <SelectItem value="horizontal">horizontal</SelectItem>  
226 - <SelectItem value="vertical">vertical</SelectItem>  
227 - </SelectContent>  
228 - </Select> 220 + <Label className="text-xs">Rotate</Label>
  221 + <div className="mt-1 flex items-center gap-2">
  222 + <Button
  223 + type="button"
  224 + variant="outline"
  225 + size="sm"
  226 + className="h-8 gap-1 px-2 text-xs"
  227 + disabled={positionLocked}
  228 + onClick={() =>
  229 + onElementChange(selectedElement.id, {
  230 + rotation: rotateElementLeftValue(selectedElement) as Rotation,
  231 + })
  232 + }
  233 + >
  234 + <RotateCcw className="h-4 w-4" />
  235 + Left
  236 + </Button>
  237 + <Button
  238 + type="button"
  239 + variant="outline"
  240 + size="sm"
  241 + className="h-8 gap-1 px-2 text-xs"
  242 + disabled={positionLocked}
  243 + onClick={() =>
  244 + onElementChange(selectedElement.id, {
  245 + rotation: rotateElementRightValue(selectedElement) as Rotation,
  246 + })
  247 + }
  248 + >
  249 + <RotateCw className="h-4 w-4" />
  250 + Right
  251 + </Button>
  252 + <span className="text-xs text-gray-500">
  253 + {elementRotationDegrees(selectedElement)}&deg;
  254 + </span>
  255 + </div>
229 </div> 256 </div>
230 ) : null} 257 ) : null}
231 {!isBlankElement ? ( 258 {!isBlankElement ? (
@@ -1194,11 +1221,20 @@ function ElementConfigFields({ @@ -1194,11 +1221,20 @@ function ElementConfigFields({
1194 ); 1221 );
1195 } 1222 }
1196 case 'TIME': 1223 case 'TIME':
  1224 + const timeFormat = cfgPickStr(cfg, ['format', 'Format'], '24 hr');
1197 return ( 1225 return (
1198 <> 1226 <>
1199 <div> 1227 <div>
1200 - <Label className="text-xs">Format</Label>  
1201 - <Input value="HH:mm" className="h-8 text-sm mt-1" readOnly /> 1228 + <Label className="text-xs">Time Format</Label>
  1229 + <Select value={timeFormat === '12 hr' ? '12 hr' : '24 hr'} onValueChange={(v) => update('format', v)}>
  1230 + <SelectTrigger className="h-8 text-sm mt-1">
  1231 + <SelectValue />
  1232 + </SelectTrigger>
  1233 + <SelectContent>
  1234 + <SelectItem value="12 hr">12 hr - 2:00pm</SelectItem>
  1235 + <SelectItem value="24 hr">24 hr - 14:00</SelectItem>
  1236 + </SelectContent>
  1237 + </Select>
1202 </div> 1238 </div>
1203 <FontFamilyField cfg={cfg} update={update} /> 1239 <FontFamilyField cfg={cfg} update={update} />
1204 <div> 1240 <div>
美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/index.tsx
@@ -438,7 +438,8 @@ export function LabelTemplateEditor({ @@ -438,7 +438,8 @@ export function LabelTemplateEditor({
438 patch.rotation !== undefined; 438 patch.rotation !== undefined;
439 if (!geomTouched) return merged; 439 if (!geomTouched) return merged;
440 // 竖排且宽>高:先纠正宽高再 clamp,避免 x 被钳到最左边 440 // 竖排且宽>高:先纠正宽高再 clamp,避免 x 被钳到最左边
441 - if (readElementRotation(merged) === 'vertical' && merged.width > merged.height) { 441 + const mergedRotation = readElementRotation(merged);
  442 + if ((mergedRotation === 'vertical' || mergedRotation === 'right') && merged.width > merged.height) {
442 merged = canonicalElementGeometry(merged); 443 merged = canonicalElementGeometry(merged);
443 } 444 }
444 const c = clampLabelElementBox( 445 const c = clampLabelElementBox(
美国版/Food Labeling Management Platform/src/components/labels/LabelsList.tsx
@@ -42,8 +42,8 @@ import { @@ -42,8 +42,8 @@ import {
42 PaginationNext, 42 PaginationNext,
43 PaginationPrevious, 43 PaginationPrevious,
44 } from "../ui/pagination"; 44 } from "../ui/pagination";
45 -import { getLabels, getLabel, createLabel, updateLabel, deleteLabel } from "../../services/labelService";  
46 -import type { LabelDto, LabelCreateInput, LabelUpdateInput } from "../../types/label"; 45 +import { getLabels, getLabel, createLabel, batchCreateLabels, updateLabel, deleteLabel } from "../../services/labelService";
  46 +import type { LabelDto, LabelCreateInput, LabelBatchCreateItemInput, LabelUpdateInput } from "../../types/label";
47 import { getLocations } from "../../services/locationService"; 47 import { getLocations } from "../../services/locationService";
48 import { getGroups } from "../../services/groupService"; 48 import { getGroups } from "../../services/groupService";
49 import { getPartners } from "../../services/partnerService"; 49 import { getPartners } from "../../services/partnerService";
@@ -2031,7 +2031,7 @@ function LabelBulkAddPage({ @@ -2031,7 +2031,7 @@ function LabelBulkAddPage({
2031 const removeRow = (rowId: string) => 2031 const removeRow = (rowId: string) =>
2032 setRows((prev) => (prev.length <= 1 ? prev : prev.filter((row) => row.id !== rowId))); 2032 setRows((prev) => (prev.length <= 1 ? prev : prev.filter((row) => row.id !== rowId)));
2033 2033
2034 - const validateAndBuildInput = (row: BulkAddRow, index: number): LabelCreateInput | null => { 2034 + const validateAndBuildInput = (row: BulkAddRow, index: number): LabelBatchCreateItemInput | null => {
2035 const rowName = `Row ${index + 1}`; 2035 const rowName = `Row ${index + 1}`;
2036 const effectivePartnerId = effectiveScopePartnerId( 2036 const effectivePartnerId = effectiveScopePartnerId(
2037 scopeAuth.requireCompanySelection, 2037 scopeAuth.requireCompanySelection,
@@ -2092,18 +2092,42 @@ function LabelBulkAddPage({ @@ -2092,18 +2092,42 @@ function LabelBulkAddPage({
2092 }); 2092 });
2093 return null; 2093 return null;
2094 } 2094 }
  2095 + const productId = row.productId.trim();
  2096 + const labelTypeId = row.labelTypeId.trim();
  2097 + const product = products.find((x) => x.id === productId);
  2098 + const labelType = types.find((x) => x.id === labelTypeId);
  2099 + const productCodeValue = (product?.codeValue ?? "").trim();
  2100 + const templateDefaultValues = selectedTemplate
  2101 + ? collectTemplateDefaultValuesForSave(
  2102 + selectedTemplate,
  2103 + row.templateDataValues,
  2104 + row.templateDateOffsets,
  2105 + row.nutritionByElementId,
  2106 + productCodeValue,
  2107 + {
  2108 + labelName: row.labelName,
  2109 + labelTypeName: labelType?.typeName ?? labelType?.typeCode,
  2110 + },
  2111 + )
  2112 + : {};
2095 return { 2113 return {
2096 labelName: row.labelName.trim(), 2114 labelName: row.labelName.trim(),
2097 - templateCode,  
2098 locationIds: locPayload.locationIds, 2115 locationIds: locPayload.locationIds,
  2116 + locationId: locPayload.locationIds[0] ?? null,
2099 labelCategoryId: row.labelCategoryId.trim(), 2117 labelCategoryId: row.labelCategoryId.trim(),
2100 - labelTypeId: row.labelTypeId.trim(),  
2101 - productIds: [row.productId.trim()], 2118 + labelTypeId,
  2119 + productIds: [productId],
2102 labelInfoJson: null, 2120 labelInfoJson: null,
2103 state: row.state, 2121 state: row.state,
2104 appliedRegionType: regionPayload.appliedRegionType, 2122 appliedRegionType: regionPayload.appliedRegionType,
2105 regionIds: regionPayload.regionIds, 2123 regionIds: regionPayload.regionIds,
2106 groupIds: regionPayload.regionIds, 2124 groupIds: regionPayload.regionIds,
  2125 + partnerId: effectivePartnerId,
  2126 + partnerIds: effectivePartnerId ? [effectivePartnerId] : [],
  2127 + templateDefaultValues,
  2128 + templateDataValues: { ...row.templateDataValues },
  2129 + templateDateOffsets: { ...row.templateDateOffsets },
  2130 + nutritionByElementId: { ...row.nutritionByElementId },
2107 }; 2131 };
2108 }; 2132 };
2109 2133
@@ -2168,31 +2192,46 @@ function LabelBulkAddPage({ @@ -2168,31 +2192,46 @@ function LabelBulkAddPage({
2168 }; 2192 };
2169 2193
2170 const submit = async () => { 2194 const submit = async () => {
2171 - const inputs: LabelCreateInput[] = [];  
2172 - const validRows: BulkAddRow[] = []; 2195 + const inputs: LabelBatchCreateItemInput[] = [];
  2196 + if (rows.length > 500) {
  2197 + toast.error("Too many rows.", {
  2198 + description: "Bulk Add supports up to 500 rows at a time.",
  2199 + });
  2200 + return;
  2201 + }
2173 for (let i = 0; i < rows.length; i += 1) { 2202 for (let i = 0; i < rows.length; i += 1) {
2174 const input = validateAndBuildInput(rows[i], i); 2203 const input = validateAndBuildInput(rows[i], i);
2175 if (!input) return; 2204 if (!input) return;
2176 inputs.push(input); 2205 inputs.push(input);
2177 - validRows.push(rows[i]);  
2178 } 2206 }
2179 2207
2180 setSaving(true); 2208 setSaving(true);
2181 try { 2209 try {
2182 - for (const input of inputs) {  
2183 - await createLabel(input);  
2184 - }  
2185 - try {  
2186 - await saveTemplateDefaultsAfterLabels(validRows);  
2187 - } catch (e: any) {  
2188 - toast.warning("Labels created, template data failed.", {  
2189 - description: e?.message ? String(e.message) : "Please edit template data manually.", 2210 + const result = await batchCreateLabels({
  2211 + templateCode: templateCode.trim(),
  2212 + saveTemplateProductDefaults: true,
  2213 + items: inputs,
  2214 + });
  2215 + const successCount = Number(result.successCount ?? 0);
  2216 + const failCount = Number(result.failCount ?? 0);
  2217 + if (failCount > 0) {
  2218 + const firstErrors = (result.errors ?? [])
  2219 + .slice(0, 3)
  2220 + .map((err) => {
  2221 + const rowNo = err.rowNumber ? `Row ${err.rowNumber}` : "Row";
  2222 + return `${rowNo}: ${err.message || "Failed"}`;
  2223 + })
  2224 + .join("; ");
  2225 + toast.warning("Bulk Add completed with errors.", {
  2226 + description: `${successCount} created, ${failCount} failed.${firstErrors ? ` ${firstErrors}` : ""}`,
  2227 + });
  2228 + if (successCount > 0) onBack();
  2229 + } else {
  2230 + toast.success("Labels created.", {
  2231 + description: `${successCount || inputs.length} label(s) have been created successfully.`,
2190 }); 2232 });
  2233 + onBack();
2191 } 2234 }
2192 - toast.success("Labels created.", {  
2193 - description: `${inputs.length} label(s) have been created successfully.`,  
2194 - });  
2195 - onBack();  
2196 } catch (e: any) { 2235 } catch (e: any) {
2197 toast.error("Failed to create labels.", { 2236 toast.error("Failed to create labels.", {
2198 description: e?.message ? String(e.message) : "Please check the rows and try again.", 2237 description: e?.message ? String(e.message) : "Please check the rows and try again.",
美国版/Food Labeling Management Platform/src/lib/labelFormDatePreview.ts
@@ -58,6 +58,8 @@ export function formatDateByPreset(format: string, date: Date): string { @@ -58,6 +58,8 @@ export function formatDateByPreset(format: string, date: Date): string {
58 const dd = String(date.getDate()).padStart(2, "0"); 58 const dd = String(date.getDate()).padStart(2, "0");
59 const hh = String(date.getHours()).padStart(2, "0"); 59 const hh = String(date.getHours()).padStart(2, "0");
60 const min = String(date.getMinutes()).padStart(2, "0"); 60 const min = String(date.getMinutes()).padStart(2, "0");
  61 + const hour12 = date.getHours() % 12 || 12;
  62 + const ampm = date.getHours() >= 12 ? "pm" : "am";
61 const monthLong = date.toLocaleString("en-US", { month: "long" }).toUpperCase(); 63 const monthLong = date.toLocaleString("en-US", { month: "long" }).toUpperCase();
62 const dayLong = date.toLocaleString("en-US", { weekday: "long" }).toUpperCase(); 64 const dayLong = date.toLocaleString("en-US", { weekday: "long" }).toUpperCase();
63 const dayShort = date.toLocaleString("en-US", { weekday: "short" }).toUpperCase(); 65 const dayShort = date.toLocaleString("en-US", { weekday: "short" }).toUpperCase();
@@ -91,6 +93,11 @@ export function formatDateByPreset(format: string, date: Date): string { @@ -91,6 +93,11 @@ export function formatDateByPreset(format: string, date: Date): string {
91 return yyyy; 93 return yyyy;
92 case "DD MONTH YEAR (25 DECEMBER 2025)": 94 case "DD MONTH YEAR (25 DECEMBER 2025)":
93 return `${dd} ${monthLong} ${yyyy}`; 95 return `${dd} ${monthLong} ${yyyy}`;
  96 + case "12 hr":
  97 + return `${hour12}:${min}${ampm}`;
  98 + case "24 hr":
  99 + case "HH:mm":
  100 + return `${hh}:${min}`;
94 default: 101 default:
95 return format 102 return format
96 .replace(/YYYY/g, yyyy) 103 .replace(/YYYY/g, yyyy)
@@ -112,7 +119,7 @@ export function isLikelyResolvedDateTimeLiteral(raw: string | null | undefined): @@ -112,7 +119,7 @@ export function isLikelyResolvedDateTimeLiteral(raw: string | null | undefined):
112 if (/^\d{4}-\d{2}-\d{2}/.test(t)) return true; 119 if (/^\d{4}-\d{2}-\d{2}/.test(t)) return true;
113 if (/^\d{4}-\d{2}-\d{2}\s+\d{1,2}:\d{2}/.test(t)) return true; 120 if (/^\d{4}-\d{2}-\d{2}\s+\d{1,2}:\d{2}/.test(t)) return true;
114 if (/^\d{1,2}\/\d{1,2}\/\d{2,4}/.test(t)) return true; 121 if (/^\d{1,2}\/\d{1,2}\/\d{2,4}/.test(t)) return true;
115 - if (/^\d{1,2}:\d{2}(:\d{2})?$/.test(t)) return true; 122 + if (/^\d{1,2}:\d{2}(:\d{2})?(\s?[ap]m)?$/i.test(t)) return true;
116 return false; 123 return false;
117 } 124 }
118 125
@@ -206,7 +213,7 @@ export function resolveStoredPrintValueToDisplayText( @@ -206,7 +213,7 @@ export function resolveStoredPrintValueToDisplayText(
206 : format; 213 : format;
207 return formatDateByPreset(fmt, d); 214 return formatDateByPreset(fmt, d);
208 } 215 }
209 - if (type === "TIME") return formatDateByPreset("HH:mm", d); 216 + if (type === "TIME") return formatDateByPreset(dateFormatPatternForElement(el, cfg), d);
210 } 217 }
211 return s; 218 return s;
212 } 219 }
@@ -230,7 +237,7 @@ export function resolveStoredPrintValueToDisplayText( @@ -230,7 +237,7 @@ export function resolveStoredPrintValueToDisplayText(
230 return formatDateByPreset(format, d); 237 return formatDateByPreset(format, d);
231 } 238 }
232 if (type === "TIME") { 239 if (type === "TIME") {
233 - return formatDateByPreset("HH:mm", d); 240 + return formatDateByPreset(dateFormatPatternForElement(el, cfg), d);
234 } 241 }
235 return `${amount} ${unit}`; 242 return `${amount} ${unit}`;
236 } 243 }
@@ -244,7 +251,10 @@ function applyElementPrefix(cfg: Record&lt;string, unknown&gt;, body: string): string @@ -244,7 +251,10 @@ function applyElementPrefix(cfg: Record&lt;string, unknown&gt;, body: string): string
244 251
245 function dateFormatPatternForElement(el: LabelElement, cfg: Record<string, unknown>): string { 252 function dateFormatPatternForElement(el: LabelElement, cfg: Record<string, unknown>): string {
246 const type = canonicalElementType(el.type); 253 const type = canonicalElementType(el.type);
247 - if (type === "TIME") return "HH:mm"; 254 + if (type === "TIME") {
  255 + const raw = String(cfg.format ?? cfg.Format ?? "24 hr").trim();
  256 + return raw === "12 hr" ? "12 hr" : "24 hr";
  257 + }
248 const it = String(cfg.inputType ?? cfg.InputType ?? "").toLowerCase(); 258 const it = String(cfg.inputType ?? cfg.InputType ?? "").toLowerCase();
249 const raw = 259 const raw =
250 (typeof cfg.format === "string" && cfg.format.trim() 260 (typeof cfg.format === "string" && cfg.format.trim()
@@ -303,7 +313,7 @@ function resolveFromOffsetAmount( @@ -303,7 +313,7 @@ function resolveFromOffsetAmount(
303 return applyElementPrefix(cfg, formatDateByPreset(dateFormatPatternForElement(el, cfg), d)); 313 return applyElementPrefix(cfg, formatDateByPreset(dateFormatPatternForElement(el, cfg), d));
304 } 314 }
305 if (type === "TIME") { 315 if (type === "TIME") {
306 - return applyElementPrefix(cfg, formatDateByPreset("HH:mm", d)); 316 + return applyElementPrefix(cfg, formatDateByPreset(dateFormatPatternForElement(el, cfg), d));
307 } 317 }
308 return applyElementPrefix(cfg, `${amount} ${unit}`.trim()); 318 return applyElementPrefix(cfg, `${amount} ${unit}`.trim());
309 } 319 }
@@ -415,4 +425,3 @@ export function applyLiveDateTimePreviewToElements( @@ -415,4 +425,3 @@ export function applyLiveDateTimePreviewToElements(
415 return { ...el, config: cfg }; 425 return { ...el, config: cfg };
416 }); 426 });
417 } 427 }
418 -  
美国版/Food Labeling Management Platform/src/services/labelService.ts
1 import { createApiClient } from "../lib/apiClient"; 1 import { createApiClient } from "../lib/apiClient";
2 import type { 2 import type {
3 LabelCreateInput, 3 LabelCreateInput,
  4 + LabelBatchCreateInput,
  5 + LabelBatchCreateResult,
4 LabelDto, 6 LabelDto,
5 LabelGetListInput, 7 LabelGetListInput,
6 LabelUpdateInput, 8 LabelUpdateInput,
@@ -156,6 +158,92 @@ export async function createLabel(input: LabelCreateInput): Promise&lt;LabelDto&gt; { @@ -156,6 +158,92 @@ export async function createLabel(input: LabelCreateInput): Promise&lt;LabelDto&gt; {
156 return normalizeLabelDto(raw); 158 return normalizeLabelDto(raw);
157 } 159 }
158 160
  161 +function normalizeBatchCreateResult(raw: unknown): LabelBatchCreateResult {
  162 + const r = (raw && typeof raw === "object" ? raw : {}) as Record<string, unknown>;
  163 + const successItemsRaw = r.successItems ?? r.SuccessItems;
  164 + const errorsRaw = r.errors ?? r.Errors;
  165 + const successItems = Array.isArray(successItemsRaw)
  166 + ? successItemsRaw.map((x) => {
  167 + const it = (x && typeof x === "object" ? x : {}) as Record<string, unknown>;
  168 + return {
  169 + rowNumber: typeof (it.rowNumber ?? it.RowNumber) === "number" ? (it.rowNumber ?? it.RowNumber) as number : null,
  170 + labelCode: it.labelCode != null || it.LabelCode != null ? String(it.labelCode ?? it.LabelCode) : null,
  171 + labelName: it.labelName != null || it.LabelName != null ? String(it.labelName ?? it.LabelName) : null,
  172 + };
  173 + })
  174 + : [];
  175 + const errors = Array.isArray(errorsRaw)
  176 + ? errorsRaw.map((x) => {
  177 + const it = (x && typeof x === "object" ? x : {}) as Record<string, unknown>;
  178 + return {
  179 + rowNumber: typeof (it.rowNumber ?? it.RowNumber) === "number" ? (it.rowNumber ?? it.RowNumber) as number : null,
  180 + labelName: it.labelName != null || it.LabelName != null ? String(it.labelName ?? it.LabelName) : null,
  181 + message: it.message != null || it.Message != null ? String(it.message ?? it.Message) : null,
  182 + };
  183 + })
  184 + : [];
  185 + return {
  186 + successCount: Number(r.successCount ?? r.SuccessCount ?? successItems.length) || 0,
  187 + failCount: Number(r.failCount ?? r.FailCount ?? errors.length) || 0,
  188 + successItems,
  189 + errors,
  190 + };
  191 +}
  192 +
  193 +export async function batchCreateLabels(input: LabelBatchCreateInput): Promise<LabelBatchCreateResult> {
  194 + const raw = await api.requestJson<unknown>({
  195 + path: `${PATH}/batch-create`,
  196 + method: "POST",
  197 + body: {
  198 + templateCode: input.templateCode,
  199 + saveTemplateProductDefaults: input.saveTemplateProductDefaults ?? true,
  200 + items: input.items.map((item) => {
  201 + const locationIds = [
  202 + ...new Set((item.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean)),
  203 + ];
  204 + const regionIds = [
  205 + ...new Set(
  206 + [...(item.regionIds ?? []), ...(item.groupIds ?? [])]
  207 + .map((x) => String(x).trim())
  208 + .filter(Boolean),
  209 + ),
  210 + ];
  211 + const partnerIds = [
  212 + ...new Set(
  213 + [
  214 + ...(item.partnerId ? [item.partnerId] : []),
  215 + ...(item.partnerIds ?? []),
  216 + ]
  217 + .map((x) => String(x).trim())
  218 + .filter(Boolean),
  219 + ),
  220 + ];
  221 + return {
  222 + labelCode: String(item.labelCode ?? "").trim() || null,
  223 + labelName: item.labelName,
  224 + labelCategoryId: item.labelCategoryId,
  225 + labelTypeId: String(item.labelTypeId ?? "").trim() || null,
  226 + productIds: item.productIds,
  227 + locationIds,
  228 + locationId: item.locationId ?? locationIds[0] ?? null,
  229 + appliedRegionType: item.appliedRegionType,
  230 + regionIds,
  231 + groupIds: regionIds,
  232 + partnerId: item.partnerId ?? partnerIds[0] ?? null,
  233 + partnerIds,
  234 + labelInfoJson: item.labelInfoJson ?? null,
  235 + state: item.state ?? true,
  236 + templateDefaultValues: item.templateDefaultValues ?? {},
  237 + templateDataValues: item.templateDataValues ?? {},
  238 + templateDateOffsets: item.templateDateOffsets ?? {},
  239 + nutritionByElementId: item.nutritionByElementId ?? {},
  240 + };
  241 + }),
  242 + },
  243 + });
  244 + return normalizeBatchCreateResult(raw);
  245 +}
  246 +
159 export async function updateLabel(labelCode: string, input: LabelUpdateInput): Promise<LabelDto> { 247 export async function updateLabel(labelCode: string, input: LabelUpdateInput): Promise<LabelDto> {
160 const raw = await api.requestJson<unknown>({ 248 const raw = await api.requestJson<unknown>({
161 path: `${PATH}/${encodeURIComponent(labelCode)}`, 249 path: `${PATH}/${encodeURIComponent(labelCode)}`,
美国版/Food Labeling Management Platform/src/types/label.ts
@@ -75,6 +75,57 @@ export type LabelCreateInput = { @@ -75,6 +75,57 @@ export type LabelCreateInput = {
75 groupIds?: string[] | null; 75 groupIds?: string[] | null;
76 }; 76 };
77 77
  78 +export type LabelBatchDateOffsetInput = {
  79 + unit: string;
  80 + value: string;
  81 +};
  82 +
  83 +export type LabelBatchCreateItemInput = {
  84 + labelCode?: string | null;
  85 + labelName: string;
  86 + labelCategoryId: string;
  87 + labelTypeId?: string | null;
  88 + productIds: string[];
  89 + locationIds?: string[] | null;
  90 + locationId?: string | null;
  91 + appliedRegionType?: "ALL" | "SPECIFIED" | string | null;
  92 + regionIds?: string[] | null;
  93 + groupIds?: string[] | null;
  94 + partnerId?: string | null;
  95 + partnerIds?: string[] | null;
  96 + labelInfoJson?: Record<string, unknown> | null;
  97 + state?: boolean;
  98 + templateDefaultValues?: Record<string, unknown>;
  99 + templateDataValues?: Record<string, string>;
  100 + templateDateOffsets?: Record<string, LabelBatchDateOffsetInput>;
  101 + nutritionByElementId?: Record<string, Record<string, string>>;
  102 +};
  103 +
  104 +export type LabelBatchCreateInput = {
  105 + templateCode: string;
  106 + saveTemplateProductDefaults?: boolean;
  107 + items: LabelBatchCreateItemInput[];
  108 +};
  109 +
  110 +export type LabelBatchCreateSuccessItem = {
  111 + rowNumber?: number | null;
  112 + labelCode?: string | null;
  113 + labelName?: string | null;
  114 +};
  115 +
  116 +export type LabelBatchCreateErrorItem = {
  117 + rowNumber?: number | null;
  118 + labelName?: string | null;
  119 + message?: string | null;
  120 +};
  121 +
  122 +export type LabelBatchCreateResult = {
  123 + successCount: number;
  124 + failCount: number;
  125 + successItems: LabelBatchCreateSuccessItem[];
  126 + errors: LabelBatchCreateErrorItem[];
  127 +};
  128 +
78 export type LabelUpdateInput = { 129 export type LabelUpdateInput = {
79 labelName: string; 130 labelName: string;
80 templateCode: string; 131 templateCode: string;
美国版/Food Labeling Management Platform/src/types/labelTemplate.ts
@@ -8,7 +8,7 @@ export type LabelType = &#39;PRICE&#39; | &#39;NUTRITION&#39; | &#39;SHIPPING&#39;; @@ -8,7 +8,7 @@ export type LabelType = &#39;PRICE&#39; | &#39;NUTRITION&#39; | &#39;SHIPPING&#39;;
8 export type Unit = 'cm' | 'inch'; 8 export type Unit = 'cm' | 'inch';
9 /** 与接口文档一致:ALL=全部门店,SPECIFIED=指定门店(需 appliedLocationIds) */ 9 /** 与接口文档一致:ALL=全部门店,SPECIFIED=指定门店(需 appliedLocationIds) */
10 export type AppliedLocation = 'ALL' | 'SPECIFIED'; 10 export type AppliedLocation = 'ALL' | 'SPECIFIED';
11 -export type Rotation = 'horizontal' | 'vertical'; 11 +export type Rotation = 'horizontal' | 'vertical' | 'left' | 'right' | 'rotate180';
12 12
13 /** 模板打印方向:vertical=竖打(默认,内容不旋转);horizontal=横打(纸张 W×H 不变,内容整体旋转 90°) */ 13 /** 模板打印方向:vertical=竖打(默认,内容不旋转);horizontal=横打(纸张 W×H 不变,内容整体旋转 90°) */
14 export type PrintOrientation = 'horizontal' | 'vertical'; 14 export type PrintOrientation = 'horizontal' | 'vertical';
@@ -659,7 +659,7 @@ export function createDefaultElement(type: ElementType, x = 20, y = 20): LabelEl @@ -659,7 +659,7 @@ export function createDefaultElement(type: ElementType, x = 20, y = 20): LabelEl
659 height: 24, 659 height: 24,
660 config: { format: 'DD/MM/YYYY', offsetDays: 0, fontFamily: editorFont, fontSize: 14, textAlign: 'left' }, 660 config: { format: 'DD/MM/YYYY', offsetDays: 0, fontFamily: editorFont, fontSize: 14, textAlign: 'left' },
661 }, 661 },
662 - TIME: { width: 100, height: 24, config: { format: 'HH:mm', offsetDays: 0, fontFamily: editorFont, fontSize: 14, textAlign: 'left' } }, 662 + TIME: { width: 100, height: 24, config: { format: '24 hr', offsetDays: 0, fontFamily: editorFont, fontSize: 14, textAlign: 'left' } },
663 DURATION: { 663 DURATION: {
664 width: 120, 664 width: 120,
665 height: 24, 665 height: 24,
美国版/Food Labeling Management Platform/src/utils/textElementLayout.ts
@@ -75,9 +75,44 @@ export function readElementBorder( @@ -75,9 +75,44 @@ export function readElementBorder(
75 /** 读取元素 rotation(兼容大小写 / 旧数据) */ 75 /** 读取元素 rotation(兼容大小写 / 旧数据) */
76 export function readElementRotation( 76 export function readElementRotation(
77 el: { rotation?: string | null }, 77 el: { rotation?: string | null },
78 -): 'horizontal' | 'vertical' { 78 +): 'horizontal' | 'vertical' | 'right' | 'rotate180' {
79 const v = String(el.rotation ?? 'horizontal').trim().toLowerCase(); 79 const v = String(el.rotation ?? 'horizontal').trim().toLowerCase();
80 - return v === 'vertical' ? 'vertical' : 'horizontal'; 80 + if (v === 'vertical' || v === 'left' || v === 'rotate-left' || v === 'left90') return 'vertical';
  81 + if (v === 'right' || v === 'rotate-right' || v === 'right90') return 'right';
  82 + if (v === 'rotate180' || v === '180' || v === 'down' || v === 'inverted') return 'rotate180';
  83 + return 'horizontal';
  84 +}
  85 +
  86 +export function elementRotationDegrees(
  87 + el: { rotation?: string | null },
  88 +): 0 | -90 | 90 | 180 {
  89 + const rot = readElementRotation(el);
  90 + if (rot === 'vertical') return -90;
  91 + if (rot === 'right') return 90;
  92 + if (rot === 'rotate180') return 180;
  93 + return 0;
  94 +}
  95 +
  96 +export function rotateElementLeftValue(
  97 + el: { rotation?: string | null },
  98 +): 'horizontal' | 'vertical' | 'right' | 'rotate180' {
  99 + const current = elementRotationDegrees(el);
  100 + const next = (((current - 90) % 360) + 360) % 360;
  101 + if (next === 270) return 'vertical';
  102 + if (next === 180) return 'rotate180';
  103 + if (next === 90) return 'right';
  104 + return 'horizontal';
  105 +}
  106 +
  107 +export function rotateElementRightValue(
  108 + el: { rotation?: string | null },
  109 +): 'horizontal' | 'vertical' | 'right' | 'rotate180' {
  110 + const current = elementRotationDegrees(el);
  111 + const next = (((current + 90) % 360) + 360) % 360;
  112 + if (next === 270) return 'vertical';
  113 + if (next === 180) return 'rotate180';
  114 + if (next === 90) return 'right';
  115 + return 'horizontal';
81 } 116 }
82 117
83 /** 与 App lineHeightForTextElement 一致:leading-tight ≈ 1.25 */ 118 /** 与 App lineHeightForTextElement 一致:leading-tight ≈ 1.25 */
@@ -124,18 +159,19 @@ export function fitFontSizeToInnerWidthEstimate( @@ -124,18 +159,19 @@ export function fitFontSizeToInnerWidthEstimate(
124 /** 切换 horizontal / vertical 时交换宽高并保持中心点不变(与参考图 2 一致) */ 159 /** 切换 horizontal / vertical 时交换宽高并保持中心点不变(与参考图 2 一致) */
125 export function patchElementRotationWithLayout( 160 export function patchElementRotationWithLayout(
126 el: { x: number; y: number; width: number; height: number; rotation?: string | null }, 161 el: { x: number; y: number; width: number; height: number; rotation?: string | null },
127 - nextRotation: 'horizontal' | 'vertical', 162 + nextRotation: 'horizontal' | 'vertical' | 'right' | 'rotate180',
128 ): { 163 ): {
129 - rotation: 'horizontal' | 'vertical'; 164 + rotation: 'horizontal' | 'vertical' | 'right' | 'rotate180';
130 x: number; 165 x: number;
131 y: number; 166 y: number;
132 width: number; 167 width: number;
133 height: number; 168 height: number;
134 } { 169 } {
135 const prev = readElementRotation(el); 170 const prev = readElementRotation(el);
  171 + const shouldSwapBox = (rotation: string) => rotation === 'vertical' || rotation === 'right';
136 if (prev === nextRotation) { 172 if (prev === nextRotation) {
137 // vertical 但宽>高:选框与文字方向不一致,强制纠正 173 // vertical 但宽>高:选框与文字方向不一致,强制纠正
138 - if (nextRotation === 'vertical' && el.width > el.height) { 174 + if (shouldSwapBox(nextRotation) && el.width > el.height) {
139 const w = Math.max(1, el.width); 175 const w = Math.max(1, el.width);
140 const h = Math.max(1, el.height); 176 const h = Math.max(1, el.height);
141 const cx = el.x + w / 2; 177 const cx = el.x + w / 2;
@@ -158,6 +194,15 @@ export function patchElementRotationWithLayout( @@ -158,6 +194,15 @@ export function patchElementRotationWithLayout(
158 height: el.height, 194 height: el.height,
159 }; 195 };
160 } 196 }
  197 + if (!shouldSwapBox(prev) && !shouldSwapBox(nextRotation)) {
  198 + return {
  199 + rotation: nextRotation,
  200 + x: el.x,
  201 + y: el.y,
  202 + width: el.width,
  203 + height: el.height,
  204 + };
  205 + }
161 const w = Math.max(1, el.width); 206 const w = Math.max(1, el.width);
162 const h = Math.max(1, el.height); 207 const h = Math.max(1, el.height);
163 const cx = el.x + w / 2; 208 const cx = el.x + w / 2;
@@ -184,11 +229,12 @@ export function canonicalElementGeometry&lt; @@ -184,11 +229,12 @@ export function canonicalElementGeometry&lt;
184 export function normalizeElementRotationBox< 229 export function normalizeElementRotationBox<
185 T extends { rotation?: string | null; width: number; height: number; x: number; y: number }, 230 T extends { rotation?: string | null; width: number; height: number; x: number; y: number },
186 >(el: T): T { 231 >(el: T): T {
187 - if (readElementRotation(el) !== 'vertical') return el; 232 + const rotation = readElementRotation(el);
  233 + if (rotation !== 'vertical' && rotation !== 'right') return el;
188 if (el.width <= el.height) return el; 234 if (el.width <= el.height) return el;
189 return { 235 return {
190 ...el, 236 ...el,
191 - rotation: 'vertical',  
192 - ...patchElementRotationWithLayout({ ...el, rotation: 'horizontal' }, 'vertical'), 237 + rotation,
  238 + ...patchElementRotationWithLayout({ ...el, rotation: 'horizontal' }, rotation),
193 }; 239 };
194 } 240 }