diff --git a/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/api/th/th-app-auth.ts b/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/api/th/th-app-auth.ts new file mode 100644 index 0000000..4d2f229 --- /dev/null +++ b/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/api/th/th-app-auth.ts @@ -0,0 +1,84 @@ +export interface ThAppBoundLocationDto { + id: string; + locationCode: string; + locationName: string; + fullAddress: string; + state: boolean; +} + +export interface ThAppLoginInput { + tenantId: string; + email: string; + password: string; + uuid?: string; + code?: string; +} + +export interface ThAppLoginOutputDto { + token: string; + refreshToken: string; + tenantId: string; + tenantName: string; + locations: ThAppBoundLocationDto[]; +} + +function normalizeLocation(raw: Record): ThAppBoundLocationDto { + return { + id: String(raw.id ?? raw.Id ?? ''), + locationCode: String(raw.locationCode ?? raw.LocationCode ?? ''), + locationName: String(raw.locationName ?? raw.LocationName ?? ''), + fullAddress: String(raw.fullAddress ?? raw.FullAddress ?? ''), + state: raw.state !== false && raw.State !== false, + }; +} + +function normalizeAppLoginOutput(raw: unknown): ThAppLoginOutputDto { + const o = + raw && typeof raw === 'object' ? (raw as Record) : {}; + const locs = o.locations ?? o.Locations; + const arr = Array.isArray(locs) ? locs : []; + return { + token: String(o.token ?? o.Token ?? ''), + refreshToken: String(o.refreshToken ?? o.RefreshToken ?? ''), + tenantId: String(o.tenantId ?? o.TenantId ?? ''), + tenantName: String(o.tenantName ?? o.TenantName ?? ''), + locations: arr.map((x) => normalizeLocation(x as Record)), + }; +} + +function normalizeLocationList(raw: unknown): ThAppBoundLocationDto[] { + const arr = Array.isArray(raw) ? raw : []; + return arr.map((x) => normalizeLocation(x as Record)); +} + +/** POST /api/app/th-app-auth/login */ +export async function thAppLogin( + input: ThAppLoginInput, +): Promise { + const { requestClient } = await import('#/api/request'); + const raw = await requestClient.post( + 'th-app-auth/login', + { + tenantId: input.tenantId, + email: input.email.trim(), + password: input.password, + ...(input.uuid ? { uuid: input.uuid } : {}), + ...(input.code != null && input.code !== '' ? { code: input.code } : {}), + }, + { + errorMessageMode: 'none', + headers: { + __tenant: input.tenantId, + }, + successMessageMode: 'none', + }, + ); + return normalizeAppLoginOutput(raw); +} + +/** GET /api/app/th-app-auth/my-locations */ +export async function thAppMyLocations(): Promise { + const { requestClient } = await import('#/api/request'); + const raw = await requestClient.get('th-app-auth/my-locations'); + return normalizeLocationList(raw); +} diff --git a/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/api/th/th-multi-tenancy.ts b/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/api/th/th-multi-tenancy.ts new file mode 100644 index 0000000..433a433 --- /dev/null +++ b/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/api/th/th-multi-tenancy.ts @@ -0,0 +1,62 @@ +import { requestClient } from '#/api/request'; + +export interface ThTenantSelectDto { + id: string; + name: string; +} + +export interface ThCurrentTenantDto { + tenantId: string | null; + tenantName: string | null; +} + +function normalizeTenantSelectItem( + raw: Record, +): ThTenantSelectDto { + return { + id: String(raw.id ?? raw.Id ?? ''), + name: String(raw.name ?? raw.Name ?? ''), + }; +} + +function normalizeTenantSelectList(raw: unknown): ThTenantSelectDto[] { + const arr = Array.isArray(raw) ? raw : []; + return arr.map((x) => + normalizeTenantSelectItem(x as Record), + ); +} + +function normalizeCurrentTenant(raw: unknown): ThCurrentTenantDto { + const o = + raw && typeof raw === 'object' ? (raw as Record) : {}; + const tenantId = o.tenantId ?? o.TenantId; + const tenantName = o.tenantName ?? o.TenantName; + return { + tenantId: tenantId == null ? null : String(tenantId), + tenantName: tenantName == null ? null : String(tenantName), + }; +} + +/** GET /api/app/th-multi-tenancy/tenant-select */ +export async function thTenantSelectList(): Promise { + const raw = await requestClient.get( + 'th-multi-tenancy/tenant-select', + { + errorMessageMode: 'none', + successMessageMode: 'none', + }, + ); + return normalizeTenantSelectList(raw); +} + +/** GET /api/app/th-multi-tenancy/current-tenant */ +export async function thCurrentTenant(): Promise { + const raw = await requestClient.get( + 'th-multi-tenancy/current-tenant', + { + errorMessageMode: 'none', + successMessageMode: 'none', + }, + ); + return normalizeCurrentTenant(raw); +} diff --git a/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/api/th/th-tenant-provisioning.ts b/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/api/th/th-tenant-provisioning.ts index 5bf5df0..66c5117 100644 --- a/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/api/th/th-tenant-provisioning.ts +++ b/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/api/th/th-tenant-provisioning.ts @@ -15,6 +15,10 @@ export interface ThProvisionTenantOutputDto { databaseInitialized: boolean; } +export interface ThInitializeTenantDatabaseInput { + tenantId: string; +} + function normalizeProvisionTenantOutput( raw: unknown, ): ThProvisionTenantOutputDto { @@ -50,3 +54,19 @@ export async function thProvisionTenant( ); return normalizeProvisionTenantOutput(raw); } + +/** POST /api/app/th-tenant-provisioning/initialize-tenant-database */ +export async function thInitializeTenantDatabase( + input: ThInitializeTenantDatabaseInput, +) { + return requestClient.post( + 'th-tenant-provisioning/initialize-tenant-database', + null, + { + params: { + tenantId: input.tenantId, + }, + successMessageMode: 'message', + }, + ); +} diff --git a/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/api/th/th-web-auth.ts b/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/api/th/th-web-auth.ts index 49169f8..9335c26 100644 --- a/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/api/th/th-web-auth.ts +++ b/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/api/th/th-web-auth.ts @@ -18,8 +18,9 @@ let loginInFlight: Promise | null = null; function normalizeWebLoginOutput(raw: unknown): ThWebLoginOutputDto { const o = raw && typeof raw === 'object' ? (raw as Record) : {}; + const token = String(o.token ?? o.Token ?? '').trim(); return { - token: String(o.token ?? o.Token ?? ''), + token: token.replace(/^Bearer\s+/i, ''), refreshToken: String(o.refreshToken ?? o.RefreshToken ?? ''), tenantId: String(o.tenantId ?? o.TenantId ?? ''), tenantName: String(o.tenantName ?? o.TenantName ?? ''), diff --git a/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/store/th-tenant.ts b/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/store/th-tenant.ts new file mode 100644 index 0000000..6a024ce --- /dev/null +++ b/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/store/th-tenant.ts @@ -0,0 +1,33 @@ +import type { ThAppBoundLocationDto } from '#/api/th'; + +import { defineStore } from 'pinia'; + +export interface ThTenantContext { + tenantId: string; + tenantName: string; + locations: ThAppBoundLocationDto[]; +} + +export const useThTenantStore = defineStore('th-tenant', { + actions: { + setTenantContext(ctx: Partial) { + if (ctx.tenantId != null) { + this.tenantId = ctx.tenantId; + } + if (ctx.tenantName != null) { + this.tenantName = ctx.tenantName; + } + if (ctx.locations != null) { + this.locations = ctx.locations; + } + }, + }, + persist: { + pick: ['tenantId', 'tenantName', 'locations'], + }, + state: (): ThTenantContext => ({ + tenantId: '', + tenantName: '', + locations: [], + }), +}); diff --git a/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/views/_core/authentication/login.vue b/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/views/_core/authentication/login.vue index 9d187b7..15c600d 100644 --- a/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/views/_core/authentication/login.vue +++ b/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/views/_core/authentication/login.vue @@ -34,6 +34,7 @@ const captchaInfo = ref({ const captchaLoading = ref(false); const tenantOptions = ref([]); const loginSubmitting = ref(false); +const LAST_TENANT_KEY = 'th:last-tenant-id'; async function loadCaptcha() { try { @@ -54,9 +55,13 @@ async function loadTenant() { try { const list = await thTenantSelectList(); tenantOptions.value = list; - const firstId = list[0]?.id ?? DEFAULT_TENANT_ID; - loginFormRef.value?.getFormApi().setFieldValue('tenantId', firstId); - loginTenantId.value = firstId; + const lastTenantId = localStorage.getItem(LAST_TENANT_KEY); + const selectedId = + list.find((item) => item.id === lastTenantId)?.id ?? + list[0]?.id ?? + DEFAULT_TENANT_ID; + loginFormRef.value?.getFormApi().setFieldValue('tenantId', selectedId); + loginTenantId.value = selectedId; } catch (error) { console.error(error); tenantOptions.value = []; @@ -153,6 +158,7 @@ async function handleAccountLogin(values: LoginAndRegisterParams) { ? { code: String(values.code ?? ''), uuid: captchaInfo.value.uuid } : {}), }); + localStorage.setItem(LAST_TENANT_KEY, String(values.tenantId ?? '')); } catch (error) { console.error(error); if (error instanceof Error && captchaInfo.value.isEnableCaptcha) { diff --git a/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/views/food-labeling/platform/tenants/data.ts b/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/views/food-labeling/platform/tenants/data.ts index 779bc21..ad7d280 100644 --- a/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/views/food-labeling/platform/tenants/data.ts +++ b/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/views/food-labeling/platform/tenants/data.ts @@ -9,27 +9,27 @@ export const querySchema: FormSchemaGetter = () => [...keywordFilterSchema()]; export const columns: VxeGridProps['columns'] = [ { field: 'companyName', - title: '租户名称', minWidth: 180, slots: { default: 'companyName' }, + title: '租户名称', }, { field: 'companyCode', - title: '租户 ID', minWidth: 260, slots: { default: 'tenantId' }, + title: '租户 ID', }, { field: 'remark', - title: '连接串 / 数据库', minWidth: 240, slots: { default: 'connectionString' }, + title: '连接串 / 数据库', }, { field: 'creationTime', - title: '创建时间', minWidth: 160, slots: { default: 'creationTime' }, + title: '创建时间', }, foodLabelingActionColumn({ minWidth: 120 }), ]; diff --git a/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/views/food-labeling/platform/tenants/index.vue b/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/views/food-labeling/platform/tenants/index.vue index 9257c8e..00b7dd8 100644 --- a/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/views/food-labeling/platform/tenants/index.vue +++ b/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/views/food-labeling/platform/tenants/index.vue @@ -84,7 +84,7 @@ async function handleDelete(row: SaasTenantDto) { function confirmDelete(row: SaasTenantDto) { Modal.confirm({ okType: 'danger', - title: $t('foodLabeling.platform.deleteCompanyConfirm'), + title: '确认删除该租户?', onOk: () => handleDelete(row), }); } @@ -94,23 +94,25 @@ function confirmDelete(row: SaasTenantDto) { diff --git a/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/views/food-labeling/platform/tenants/tenant-modal.vue b/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/views/food-labeling/platform/tenants/tenant-modal.vue index 294b056..d20f968 100644 --- a/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/views/food-labeling/platform/tenants/tenant-modal.vue +++ b/泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/views/food-labeling/platform/tenants/tenant-modal.vue @@ -20,16 +20,14 @@ const entityVersion = ref(); const dbType = ref(); const title = computed(() => - isUpdate.value - ? $t('foodLabeling.platform.editCompany') - : $t('foodLabeling.platform.addCompany'), + isUpdate.value ? '编辑租户' : '开通租户', ); const [BasicForm, formApi] = useVbenForm({ commonConfig: { + componentProps: { class: 'w-full' }, formItemClass: 'col-span-2', labelWidth: 120, - componentProps: { class: 'w-full' }, }, schema: [ { @@ -40,23 +38,23 @@ const [BasicForm, formApi] = useVbenForm({ }, { component: 'Input', - fieldName: 'tenantConnectionString', - label: '自定义连接串', - formItemClass: 'col-span-2', componentProps: { placeholder: '可选;为空时后端按租户名称自动生成业务库连接串', }, + fieldName: 'tenantConnectionString', + formItemClass: 'col-span-2', + label: '自定义连接串', }, { component: 'Switch', - fieldName: 'initializeDatabase', - label: '初始化数据库', - defaultValue: true, componentProps: { class: 'w-fit' }, + defaultValue: true, dependencies: { if: () => !isUpdate.value, triggerFields: ['companyName'], }, + fieldName: 'initializeDatabase', + label: '初始化数据库', }, ], showDefaultActions: false, @@ -103,26 +101,29 @@ async function handleConfirm() { return; } const values = cloneDeep(await formApi.getValues()) as Record; + const name = String(values.companyName ?? '').trim(); + const tenantConnectionString = String( + values.tenantConnectionString ?? '', + ).trim(); + if (isUpdate.value && recordId.value) { await saasTenantUpdate({ dbType: dbType.value, entityVersion: entityVersion.value, id: recordId.value, - name: String(values.companyName ?? '').trim(), - tenantConnectionString: String( - values.tenantConnectionString ?? '', - ).trim(), + name, + tenantConnectionString, }); } else { - await thProvisionTenant({ + const result = await thProvisionTenant({ dbType: 0, initializeDatabase: values.initializeDatabase !== false, - name: String(values.companyName ?? '').trim(), - tenantConnectionString: String( - values.tenantConnectionString ?? '', - ).trim(), + name, + tenantConnectionString, }); + console.info('TH tenant provisioned', result); } + message.success($t('http.operationSuccess')); emit('reload'); await handleCancel(); @@ -142,7 +143,7 @@ async function handleCancel() { diff --git a/美国版/Food Labeling Management App UniApp/src/utils/appAdminRole.ts b/美国版/Food Labeling Management App UniApp/src/utils/appAdminRole.ts index da000a3..1bfa47c 100644 --- a/美国版/Food Labeling Management App UniApp/src/utils/appAdminRole.ts +++ b/美国版/Food Labeling Management App UniApp/src/utils/appAdminRole.ts @@ -4,10 +4,13 @@ import type { UsAppMyProfileOutputDto } from '../services/usAppAuth' export function isAppAdminUser(profile: UsAppMyProfileOutputDto | null | undefined): boolean { if (!profile) return false const code = (profile.primaryRoleCode ?? '').trim().toLowerCase() + const compactCode = code.replace(/[\s_-]+/g, '') if (code === 'admin') return true + if (compactCode === 'companyadmin') return true const display = (profile.roleDisplay ?? '').trim().toLowerCase() if (!display) return false if (display.includes('administrator')) return true if (display.includes('super admin')) return true + if (display.includes('company admin')) return true return false } diff --git a/美国版/Food Labeling Management Platform/src/components/labels/LabelCategoriesView.tsx b/美国版/Food Labeling Management Platform/src/components/labels/LabelCategoriesView.tsx index c34b89f..51c1ca0 100755 --- a/美国版/Food Labeling Management Platform/src/components/labels/LabelCategoriesView.tsx +++ b/美国版/Food Labeling Management Platform/src/components/labels/LabelCategoriesView.tsx @@ -143,6 +143,8 @@ function locationsForCategoryAvailability(c: LabelCategoryDto, catalog: Location } function categoryRegionSummary(c: LabelCategoryDto, catalog: LocationDto[]): string { + const apiRegion = (c.region ?? "").trim(); + if (apiRegion) return apiRegion; const at = String(c.availabilityType ?? "ALL").trim().toUpperCase(); if (at !== "SPECIFIED") return "All"; const matched = locationsForCategoryAvailability(c, catalog); @@ -152,6 +154,8 @@ function categoryRegionSummary(c: LabelCategoryDto, catalog: LocationDto[]): str } function categoryLocationLines(c: LabelCategoryDto, catalog: LocationDto[]): string[] { + const apiLoc = (c.location ?? "").trim(); + if (apiLoc) return apiLoc.split(/\s*;\s*/).map((x) => x.trim()).filter(Boolean); const at = String(c.availabilityType ?? "ALL").trim().toUpperCase(); if (at !== "SPECIFIED") return ["All"]; const matched = locationsForCategoryAvailability(c, catalog); diff --git a/美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/LabelCanvas.tsx b/美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/LabelCanvas.tsx index 6a79c81..1670904 100644 --- a/美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/LabelCanvas.tsx +++ b/美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/LabelCanvas.tsx @@ -372,7 +372,9 @@ export function clampLabelElementBox( baseH: number, marginPx: number = LABEL_CANVAS_SAFE_MARGIN_PX, orientation: PrintOrientation = 'vertical', + snap: boolean = true, ): { x: number; y: number; w: number; h: number } { + const align = snap ? snapToGrid : (value: number) => value; if (orientation === 'horizontal') { const cx = baseW / 2; const cy = baseH / 2; @@ -384,21 +386,21 @@ export function clampLabelElementBox( return { x: minX, y: minY, - w: Math.max(GRID_SIZE, snapToGrid(innerW)), - h: Math.max(GRID_SIZE, snapToGrid(innerH)), + w: Math.max(GRID_SIZE, align(innerW)), + h: Math.max(GRID_SIZE, align(innerH)), }; } - let cw = Math.min(Math.max(20, snapToGrid(w)), snapToGrid(innerW)); - let ch = Math.min(Math.max(12, snapToGrid(h)), snapToGrid(innerH)); + let cw = Math.min(Math.max(20, align(w)), align(innerW)); + let ch = Math.min(Math.max(12, align(h)), align(innerH)); const maxX = baseH - marginPx + cx - cy - cw; const maxY = cx + cy - marginPx - ch; if (maxX < minX || maxY < minY) { - return { x: minX, y: minY, w: snapToGrid(innerW), h: snapToGrid(innerH) }; + return { x: minX, y: minY, w: align(innerW), h: align(innerH) }; } - let nx = snapToGrid(x); - let ny = snapToGrid(y); - nx = Math.min(Math.max(nx, minX), snapToGrid(maxX)); - ny = Math.min(Math.max(ny, minY), snapToGrid(maxY)); + let nx = align(x); + let ny = align(y); + nx = Math.min(Math.max(nx, minX), align(maxX)); + ny = Math.min(Math.max(ny, minY), align(maxY)); return { x: nx, y: ny, w: cw, h: ch }; } @@ -406,22 +408,22 @@ export function clampLabelElementBox( const minY = marginPx; const maxR = baseW - marginPx; const maxB = baseH - marginPx; - let cw = Math.max(20, snapToGrid(w)); - let ch = Math.max(12, snapToGrid(h)); + let cw = Math.max(20, align(w)); + let ch = Math.max(12, align(h)); const innerW = maxR - minX; const innerH = maxB - minY; if (innerW < GRID_SIZE || innerH < GRID_SIZE) { return { x: minX, y: minY, - w: Math.max(GRID_SIZE, snapToGrid(innerW)), - h: Math.max(GRID_SIZE, snapToGrid(innerH)), + w: Math.max(GRID_SIZE, align(innerW)), + h: Math.max(GRID_SIZE, align(innerH)), }; } - cw = Math.min(cw, snapToGrid(innerW)); - ch = Math.min(ch, snapToGrid(innerH)); - let cx = snapToGrid(x); - let cy = snapToGrid(y); + cw = Math.min(cw, align(innerW)); + ch = Math.min(ch, align(innerH)); + let cx = align(x); + let cy = align(y); cx = Math.min(Math.max(cx, minX), Math.max(minX, maxR - cw)); cy = Math.min(Math.max(cy, minY), Math.max(minY, maxB - ch)); return { x: cx, y: cy, w: cw, h: ch }; diff --git a/美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/PropertiesPanel.tsx b/美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/PropertiesPanel.tsx index 0e3a634..9a9e232 100644 --- a/美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/PropertiesPanel.tsx +++ b/美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/PropertiesPanel.tsx @@ -14,7 +14,6 @@ import { Switch } from '../../ui/switch'; import type { LabelTemplate, LabelElement, - Unit, Rotation, Border, NutritionExtraItem, @@ -53,12 +52,10 @@ import { } from '../../../utils/textElementLayout'; import { type PreviewRulerDisplayUnit, - displayLengthToElementPx, - elementPxToDisplayLength, formatPreviewRulerDisplayValue, previewRulerUnitLabel, } from '../../../utils/previewRulerUnits'; -import { unitToPx } from '../../../utils/labelTemplateUnits'; +import { pxToUnit, unitToPx } from '../../../utils/labelTemplateUnits'; import { ImageUrlUpload } from '../../ui/image-url-upload'; import { readImageScaleMode } from '../../../utils/imageScaleMode'; import { @@ -111,7 +108,7 @@ interface PropertiesPanelProps { template: LabelTemplate; selectedElement: LabelElement | null; onTemplateChange: (patch: Partial) => void; - onElementChange: (id: string, patch: Partial) => void; + onElementChange: (id: string, patch: Partial, options?: { snapToGrid?: boolean }) => void; onDeleteElement?: (id: string) => void; /** 编辑已有模板时禁止修改 Template Code */ readOnlyTemplateCode?: boolean; @@ -186,27 +183,23 @@ export function PropertiesPanel({ onElementChange(selectedElement.id, { width: Math.max(1, width), - }) + }, { snapToGrid: false }) } /> onElementChange(selectedElement.id, { height: Math.max(1, height), - }) + }, { snapToGrid: false }) } /> @@ -549,29 +542,17 @@ function CompanyPrintFieldsEditor({ function ElementDimensionField({ label, pxValue, - paperSizeTemplate, - templateUnit, displayUnit, onPxChange, disabled = false, }: { label: string; pxValue: number; - paperSizeTemplate: number; - templateUnit: Unit; displayUnit: PreviewRulerDisplayUnit; onPxChange: (px: number) => void; disabled?: boolean; }) { - const basePaperPx = unitToPx(Number(paperSizeTemplate) || 0, templateUnit); - const paperSize = Number(paperSizeTemplate) || 0; - const displayValue = elementPxToDisplayLength( - pxValue, - basePaperPx, - paperSize, - templateUnit, - displayUnit, - ); + const displayValue = pxToUnit(pxValue, displayUnit); const formatted = formatPreviewRulerDisplayValue(displayValue, displayUnit); const unitLabel = previewRulerUnitLabel(displayUnit); const step = displayUnit === 'mm' ? 1 : displayUnit === 'inch' ? 0.0001 : 0.001; @@ -585,19 +566,11 @@ function ElementDimensionField({ if (trimmed === '' || trimmed === '-' || trimmed === '.') return false; const nextDisplay = Number(trimmed); if (!Number.isFinite(nextDisplay)) return false; - const nextPx = Math.round( - displayLengthToElementPx( - nextDisplay, - basePaperPx, - paperSize, - templateUnit, - displayUnit, - ), - ); + const nextPx = unitToPx(nextDisplay, displayUnit); onPxChange(Math.max(1, nextPx)); return true; }, - [basePaperPx, displayUnit, onPxChange, paperSize, templateUnit], + [displayUnit, onPxChange], ); return ( @@ -618,17 +591,7 @@ function ElementDimensionField({ setEditing(false); }} onChange={(e) => { - const raw = e.target.value; - setDraft(raw); - const trimmed = raw.trim(); - if ( - trimmed !== '' && - trimmed !== '-' && - !trimmed.endsWith('.') && - !trimmed.endsWith('-') - ) { - commitDisplay(raw); - } + setDraft(e.target.value); }} onKeyDown={(e) => { if (e.key === 'Enter') { @@ -1038,6 +1001,7 @@ function ElementConfigFields({ onChange={(e) => update('data', e.target.value)} className="h-8 text-sm mt-1" /> +

{DEMO_VALUE_HINT}

@@ -1089,6 +1053,7 @@ function ElementConfigFields({ onChange={(e) => update('data', e.target.value)} className="h-8 text-sm mt-1" /> +

{DEMO_VALUE_HINT}

); case 'IMAGE': { diff --git a/美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/index.tsx b/美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/index.tsx index bf7e6af..2e08262 100644 --- a/美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/index.tsx +++ b/美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/index.tsx @@ -401,7 +401,7 @@ export function LabelTemplateEditor({ /** 配置区单行 flex */ const configRowClass = "flex w-full min-w-0 flex-wrap items-center gap-2"; - const updateElement = useCallback((id: string, patch: Partial) => { + const updateElement = useCallback((id: string, patch: Partial, options?: { snapToGrid?: boolean }) => { const geomTouched = patch.x !== undefined || patch.y !== undefined || @@ -451,6 +451,7 @@ export function LabelTemplateEditor({ baseH, undefined, orientation, + options?.snapToGrid ?? true, ); return { ...merged, diff --git a/美国版/Food Labeling Management Platform/src/components/locations/LocationsView.tsx b/美国版/Food Labeling Management Platform/src/components/locations/LocationsView.tsx index 229ff19..7376b3a 100755 --- a/美国版/Food Labeling Management Platform/src/components/locations/LocationsView.tsx +++ b/美国版/Food Labeling Management Platform/src/components/locations/LocationsView.tsx @@ -139,6 +139,7 @@ export type LocationsViewProps = { }; export function LocationsView({ renderBeforeTabs, canMutateLocations = true }: LocationsViewProps = {}) { + const scopeAuth = useCategoryScopeAuth(); const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); @@ -164,6 +165,7 @@ export function LocationsView({ renderBeforeTabs, canMutateLocations = true }: L /** 筛选栏:公司 / 区域选项来自接口,而非当前门店分页结果去重 */ const [filterPartnerOptions, setFilterPartnerOptions] = useState([]); const [filterGroupOptions, setFilterGroupOptions] = useState([]); + const [filterLocationOptions, setFilterLocationOptions] = useState([]); const [pageIndex, setPageIndex] = useState(1); const [pageSize, setPageSize] = useState(10); @@ -194,6 +196,15 @@ export function LocationsView({ renderBeforeTabs, canMutateLocations = true }: L return ["all", ...names]; }, [filterPartnerOptions]); + const fixedPartnerName = useMemo(() => { + if (scopeAuth.requireCompanySelection) return ""; + const fixedPartnerId = scopeAuth.fixedPartnerId.trim(); + if (!fixedPartnerId) return ""; + return ( + filterPartnerOptions.find((p) => p.id === fixedPartnerId)?.partnerName?.trim() ?? "" + ); + }, [scopeAuth.requireCompanySelection, scopeAuth.fixedPartnerId, filterPartnerOptions]); + const groupOptions = useMemo(() => { const names = filterGroupOptions .map((g) => (g.groupName ?? "").trim()) @@ -204,12 +215,12 @@ export function LocationsView({ renderBeforeTabs, canMutateLocations = true }: L const locationOptions = useMemo(() => { const s = new Set(); - for (const x of locations) { - const v = (x.locationCode ?? "").trim(); + for (const x of filterLocationOptions) { + const v = (x.locationName ?? "").trim(); if (v) s.add(v); } return ["all", ...Array.from(s).sort((a, b) => a.localeCompare(b))]; - }, [locations]); + }, [filterLocationOptions]); const totalPages = Math.max(1, Math.ceil(total / pageSize)); @@ -227,6 +238,12 @@ export function LocationsView({ renderBeforeTabs, canMutateLocations = true }: L }, []); useEffect(() => { + if (scopeAuth.requireCompanySelection) return; + if (!fixedPartnerName || partner === fixedPartnerName) return; + setPartner(fixedPartnerName); + }, [scopeAuth.requireCompanySelection, fixedPartnerName, partner]); + + useEffect(() => { const ac = new AbortController(); (async () => { try { @@ -271,6 +288,38 @@ export function LocationsView({ renderBeforeTabs, canMutateLocations = true }: L }, [groupName, groupOptions]); useEffect(() => { + const ac = new AbortController(); + (async () => { + try { + const out: LocationDto[] = []; + const size = 100; + for (let page = 1; page <= 200; page += 1) { + const res = await getLocations( + { + skipCount: page, + maxResultCount: size, + partner: partner !== "all" ? partner : undefined, + groupName: groupName !== "all" ? groupName : undefined, + }, + ac.signal, + ); + out.push(...(res.items ?? [])); + if (!res.items || res.items.length < size) break; + } + setFilterLocationOptions(out); + } catch { + if (!ac.signal.aborted) setFilterLocationOptions([]); + } + })(); + return () => ac.abort(); + }, [partner, groupName, refreshSeq]); + + useEffect(() => { + if (locationPick === "all") return; + if (!locationOptions.includes(locationPick)) setLocationPick("all"); + }, [locationPick, locationOptions]); + + useEffect(() => { // When filter changes, reset to first page. setPageIndex(1); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -351,18 +400,20 @@ export function LocationsView({ renderBeforeTabs, canMutateLocations = true }: L style={{ height: 40, boxSizing: "border-box" }} className="border border-gray-300 rounded-md w-40 shrink-0 bg-white placeholder:text-gray-500" /> - + {scopeAuth.requireCompanySelection ? ( + + ) : null} - - - - - All Companies - {memberCompanyOptions.map((p) => ( - - {(p.partnerName ?? "").trim() || p.id} - - ))} - - + {scopeAuth.requireCompanySelection ? ( + + ) : null} - - - - - Company (All) - {groupFilterPartnerOptions.map((p) => ( - - {p.partnerName ?? p.id} - - ))} - - + {scopeAuth.requireCompanySelection ? ( + + ) : null} - - - - - All Companies - {companyOptions.map((c) => ( - - {toDisplay(c.partnerName)} - - ))} - - + {scopeAuth.requireCompanySelection ? ( + + ) : null}