Commit 40929347cadf2dac8f0d1dfc7e05f674bfb26440
1 parent
2a0e4d1d
feat: update label bulk add and template rotation
Showing
16 changed files
with
400 additions
and
81 deletions
美国版/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)}° | ||
| 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<string, unknown>, body: string): string | @@ -244,7 +251,10 @@ function applyElementPrefix(cfg: Record<string, unknown>, 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<LabelDto> { | @@ -156,6 +158,92 @@ export async function createLabel(input: LabelCreateInput): Promise<LabelDto> { | ||
| 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 = 'PRICE' | 'NUTRITION' | 'SHIPPING'; | @@ -8,7 +8,7 @@ export type LabelType = 'PRICE' | 'NUTRITION' | 'SHIPPING'; | ||
| 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< | @@ -184,11 +229,12 @@ export function canonicalElementGeometry< | ||
| 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 | } |