Commit e9fbe79677560d4246c8f6d21c5656e1bacb9a03

Authored by 杨鑫
1 parent e76e1ab9

前端优化 bug

Showing 18 changed files with 443 additions and 212 deletions
泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/api/th/th-app-auth.ts 0 → 100644
  1 +export interface ThAppBoundLocationDto {
  2 + id: string;
  3 + locationCode: string;
  4 + locationName: string;
  5 + fullAddress: string;
  6 + state: boolean;
  7 +}
  8 +
  9 +export interface ThAppLoginInput {
  10 + tenantId: string;
  11 + email: string;
  12 + password: string;
  13 + uuid?: string;
  14 + code?: string;
  15 +}
  16 +
  17 +export interface ThAppLoginOutputDto {
  18 + token: string;
  19 + refreshToken: string;
  20 + tenantId: string;
  21 + tenantName: string;
  22 + locations: ThAppBoundLocationDto[];
  23 +}
  24 +
  25 +function normalizeLocation(raw: Record<string, unknown>): ThAppBoundLocationDto {
  26 + return {
  27 + id: String(raw.id ?? raw.Id ?? ''),
  28 + locationCode: String(raw.locationCode ?? raw.LocationCode ?? ''),
  29 + locationName: String(raw.locationName ?? raw.LocationName ?? ''),
  30 + fullAddress: String(raw.fullAddress ?? raw.FullAddress ?? ''),
  31 + state: raw.state !== false && raw.State !== false,
  32 + };
  33 +}
  34 +
  35 +function normalizeAppLoginOutput(raw: unknown): ThAppLoginOutputDto {
  36 + const o =
  37 + raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : {};
  38 + const locs = o.locations ?? o.Locations;
  39 + const arr = Array.isArray(locs) ? locs : [];
  40 + return {
  41 + token: String(o.token ?? o.Token ?? ''),
  42 + refreshToken: String(o.refreshToken ?? o.RefreshToken ?? ''),
  43 + tenantId: String(o.tenantId ?? o.TenantId ?? ''),
  44 + tenantName: String(o.tenantName ?? o.TenantName ?? ''),
  45 + locations: arr.map((x) => normalizeLocation(x as Record<string, unknown>)),
  46 + };
  47 +}
  48 +
  49 +function normalizeLocationList(raw: unknown): ThAppBoundLocationDto[] {
  50 + const arr = Array.isArray(raw) ? raw : [];
  51 + return arr.map((x) => normalizeLocation(x as Record<string, unknown>));
  52 +}
  53 +
  54 +/** POST /api/app/th-app-auth/login */
  55 +export async function thAppLogin(
  56 + input: ThAppLoginInput,
  57 +): Promise<ThAppLoginOutputDto> {
  58 + const { requestClient } = await import('#/api/request');
  59 + const raw = await requestClient.post<unknown>(
  60 + 'th-app-auth/login',
  61 + {
  62 + tenantId: input.tenantId,
  63 + email: input.email.trim(),
  64 + password: input.password,
  65 + ...(input.uuid ? { uuid: input.uuid } : {}),
  66 + ...(input.code != null && input.code !== '' ? { code: input.code } : {}),
  67 + },
  68 + {
  69 + errorMessageMode: 'none',
  70 + headers: {
  71 + __tenant: input.tenantId,
  72 + },
  73 + successMessageMode: 'none',
  74 + },
  75 + );
  76 + return normalizeAppLoginOutput(raw);
  77 +}
  78 +
  79 +/** GET /api/app/th-app-auth/my-locations */
  80 +export async function thAppMyLocations(): Promise<ThAppBoundLocationDto[]> {
  81 + const { requestClient } = await import('#/api/request');
  82 + const raw = await requestClient.get<unknown>('th-app-auth/my-locations');
  83 + return normalizeLocationList(raw);
  84 +}
... ...
泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/api/th/th-multi-tenancy.ts 0 → 100644
  1 +import { requestClient } from '#/api/request';
  2 +
  3 +export interface ThTenantSelectDto {
  4 + id: string;
  5 + name: string;
  6 +}
  7 +
  8 +export interface ThCurrentTenantDto {
  9 + tenantId: string | null;
  10 + tenantName: string | null;
  11 +}
  12 +
  13 +function normalizeTenantSelectItem(
  14 + raw: Record<string, unknown>,
  15 +): ThTenantSelectDto {
  16 + return {
  17 + id: String(raw.id ?? raw.Id ?? ''),
  18 + name: String(raw.name ?? raw.Name ?? ''),
  19 + };
  20 +}
  21 +
  22 +function normalizeTenantSelectList(raw: unknown): ThTenantSelectDto[] {
  23 + const arr = Array.isArray(raw) ? raw : [];
  24 + return arr.map((x) =>
  25 + normalizeTenantSelectItem(x as Record<string, unknown>),
  26 + );
  27 +}
  28 +
  29 +function normalizeCurrentTenant(raw: unknown): ThCurrentTenantDto {
  30 + const o =
  31 + raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : {};
  32 + const tenantId = o.tenantId ?? o.TenantId;
  33 + const tenantName = o.tenantName ?? o.TenantName;
  34 + return {
  35 + tenantId: tenantId == null ? null : String(tenantId),
  36 + tenantName: tenantName == null ? null : String(tenantName),
  37 + };
  38 +}
  39 +
  40 +/** GET /api/app/th-multi-tenancy/tenant-select */
  41 +export async function thTenantSelectList(): Promise<ThTenantSelectDto[]> {
  42 + const raw = await requestClient.get<unknown>(
  43 + 'th-multi-tenancy/tenant-select',
  44 + {
  45 + errorMessageMode: 'none',
  46 + successMessageMode: 'none',
  47 + },
  48 + );
  49 + return normalizeTenantSelectList(raw);
  50 +}
  51 +
  52 +/** GET /api/app/th-multi-tenancy/current-tenant */
  53 +export async function thCurrentTenant(): Promise<ThCurrentTenantDto> {
  54 + const raw = await requestClient.get<unknown>(
  55 + 'th-multi-tenancy/current-tenant',
  56 + {
  57 + errorMessageMode: 'none',
  58 + successMessageMode: 'none',
  59 + },
  60 + );
  61 + return normalizeCurrentTenant(raw);
  62 +}
... ...
泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/api/th/th-tenant-provisioning.ts
... ... @@ -15,6 +15,10 @@ export interface ThProvisionTenantOutputDto {
15 15 databaseInitialized: boolean;
16 16 }
17 17  
  18 +export interface ThInitializeTenantDatabaseInput {
  19 + tenantId: string;
  20 +}
  21 +
18 22 function normalizeProvisionTenantOutput(
19 23 raw: unknown,
20 24 ): ThProvisionTenantOutputDto {
... ... @@ -50,3 +54,19 @@ export async function thProvisionTenant(
50 54 );
51 55 return normalizeProvisionTenantOutput(raw);
52 56 }
  57 +
  58 +/** POST /api/app/th-tenant-provisioning/initialize-tenant-database */
  59 +export async function thInitializeTenantDatabase(
  60 + input: ThInitializeTenantDatabaseInput,
  61 +) {
  62 + return requestClient.post<void>(
  63 + 'th-tenant-provisioning/initialize-tenant-database',
  64 + null,
  65 + {
  66 + params: {
  67 + tenantId: input.tenantId,
  68 + },
  69 + successMessageMode: 'message',
  70 + },
  71 + );
  72 +}
... ...
泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/api/th/th-web-auth.ts
... ... @@ -18,8 +18,9 @@ let loginInFlight: Promise&lt;ThWebLoginOutputDto&gt; | null = null;
18 18 function normalizeWebLoginOutput(raw: unknown): ThWebLoginOutputDto {
19 19 const o =
20 20 raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : {};
  21 + const token = String(o.token ?? o.Token ?? '').trim();
21 22 return {
22   - token: String(o.token ?? o.Token ?? ''),
  23 + token: token.replace(/^Bearer\s+/i, ''),
23 24 refreshToken: String(o.refreshToken ?? o.RefreshToken ?? ''),
24 25 tenantId: String(o.tenantId ?? o.TenantId ?? ''),
25 26 tenantName: String(o.tenantName ?? o.TenantName ?? ''),
... ...
泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/store/th-tenant.ts 0 → 100644
  1 +import type { ThAppBoundLocationDto } from '#/api/th';
  2 +
  3 +import { defineStore } from 'pinia';
  4 +
  5 +export interface ThTenantContext {
  6 + tenantId: string;
  7 + tenantName: string;
  8 + locations: ThAppBoundLocationDto[];
  9 +}
  10 +
  11 +export const useThTenantStore = defineStore('th-tenant', {
  12 + actions: {
  13 + setTenantContext(ctx: Partial<ThTenantContext>) {
  14 + if (ctx.tenantId != null) {
  15 + this.tenantId = ctx.tenantId;
  16 + }
  17 + if (ctx.tenantName != null) {
  18 + this.tenantName = ctx.tenantName;
  19 + }
  20 + if (ctx.locations != null) {
  21 + this.locations = ctx.locations;
  22 + }
  23 + },
  24 + },
  25 + persist: {
  26 + pick: ['tenantId', 'tenantName', 'locations'],
  27 + },
  28 + state: (): ThTenantContext => ({
  29 + tenantId: '',
  30 + tenantName: '',
  31 + locations: [],
  32 + }),
  33 +});
... ...
泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/views/_core/authentication/login.vue
... ... @@ -34,6 +34,7 @@ const captchaInfo = ref&lt;CaptchaResponse&gt;({
34 34 const captchaLoading = ref(false);
35 35 const tenantOptions = ref<ThTenantSelectDto[]>([]);
36 36 const loginSubmitting = ref(false);
  37 +const LAST_TENANT_KEY = 'th:last-tenant-id';
37 38  
38 39 async function loadCaptcha() {
39 40 try {
... ... @@ -54,9 +55,13 @@ async function loadTenant() {
54 55 try {
55 56 const list = await thTenantSelectList();
56 57 tenantOptions.value = list;
57   - const firstId = list[0]?.id ?? DEFAULT_TENANT_ID;
58   - loginFormRef.value?.getFormApi().setFieldValue('tenantId', firstId);
59   - loginTenantId.value = firstId;
  58 + const lastTenantId = localStorage.getItem(LAST_TENANT_KEY);
  59 + const selectedId =
  60 + list.find((item) => item.id === lastTenantId)?.id ??
  61 + list[0]?.id ??
  62 + DEFAULT_TENANT_ID;
  63 + loginFormRef.value?.getFormApi().setFieldValue('tenantId', selectedId);
  64 + loginTenantId.value = selectedId;
60 65 } catch (error) {
61 66 console.error(error);
62 67 tenantOptions.value = [];
... ... @@ -153,6 +158,7 @@ async function handleAccountLogin(values: LoginAndRegisterParams) {
153 158 ? { code: String(values.code ?? ''), uuid: captchaInfo.value.uuid }
154 159 : {}),
155 160 });
  161 + localStorage.setItem(LAST_TENANT_KEY, String(values.tenantId ?? ''));
156 162 } catch (error) {
157 163 console.error(error);
158 164 if (error instanceof Error && captchaInfo.value.isEnableCaptcha) {
... ...
泰额版/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 = () =&gt; [...keywordFilterSchema()];
9 9 export const columns: VxeGridProps['columns'] = [
10 10 {
11 11 field: 'companyName',
12   - title: '租户名称',
13 12 minWidth: 180,
14 13 slots: { default: 'companyName' },
  14 + title: '租户名称',
15 15 },
16 16 {
17 17 field: 'companyCode',
18   - title: '租户 ID',
19 18 minWidth: 260,
20 19 slots: { default: 'tenantId' },
  20 + title: '租户 ID',
21 21 },
22 22 {
23 23 field: 'remark',
24   - title: '连接串 / 数据库',
25 24 minWidth: 240,
26 25 slots: { default: 'connectionString' },
  26 + title: '连接串 / 数据库',
27 27 },
28 28 {
29 29 field: 'creationTime',
30   - title: '创建时间',
31 30 minWidth: 160,
32 31 slots: { default: 'creationTime' },
  32 + title: '创建时间',
33 33 },
34 34 foodLabelingActionColumn({ minWidth: 120 }),
35 35 ];
... ...
泰额版/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) {
84 84 function confirmDelete(row: SaasTenantDto) {
85 85 Modal.confirm({
86 86 okType: 'danger',
87   - title: $t('foodLabeling.platform.deleteCompanyConfirm'),
  87 + title: '确认删除该租户?',
88 88 onOk: () => handleDelete(row),
89 89 });
90 90 }
... ... @@ -94,23 +94,25 @@ function confirmDelete(row: SaasTenantDto) {
94 94 <Page :auto-content-height="true" :content-class="managementTabPageContentClass">
95 95 <Alert
96 96 class="mb-3 shrink-0"
97   - message="平台管理员:此处通过 tenant-select 查看租户,通过 provision 开通独立库租户;业务接口依赖登录 Token 或 __tenant 请求头识别租户上下文。"
  97 + message="平台管理员:此处通过 tenant-select 查看租户,通过 provision 开通独立库租户;业务接口依赖登录 Token 或 __tenant 请求头识别租户上下文。"
98 98 show-icon
99 99 type="info"
100 100 />
101 101  
102 102 <BasicTable :class="managementTabTableClass">
103 103 <template #toolbar-tools>
104   - <Button type="primary" @click="handleAdd">
105   - {{ $t('foodLabeling.platform.addCompany') }}
106   - </Button>
  104 + <Button type="primary" @click="handleAdd">开通租户</Button>
107 105 </template>
108 106  
109 107 <template #companyName="{ row }">
110 108 <div class="flex min-w-0 items-center gap-2">
111 109 <Avatar v-if="row.logoUrl" :src="row.logoUrl" size="small" />
112   - <Avatar v-else size="small">{{ (row.companyName ?? '?').slice(0, 1) }}</Avatar>
113   - <span class="truncate font-medium">{{ displayText(row.companyName) }}</span>
  110 + <Avatar v-else size="small">
  111 + {{ (row.companyName ?? '?').slice(0, 1) }}
  112 + </Avatar>
  113 + <span class="truncate font-medium">
  114 + {{ displayText(row.companyName) }}
  115 + </span>
114 116 </div>
115 117 </template>
116 118  
... ...
泰额版/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&lt;number | undefined&gt;();
20 20 const dbType = ref<number | undefined>();
21 21  
22 22 const title = computed(() =>
23   - isUpdate.value
24   - ? $t('foodLabeling.platform.editCompany')
25   - : $t('foodLabeling.platform.addCompany'),
  23 + isUpdate.value ? '编辑租户' : '开通租户',
26 24 );
27 25  
28 26 const [BasicForm, formApi] = useVbenForm({
29 27 commonConfig: {
  28 + componentProps: { class: 'w-full' },
30 29 formItemClass: 'col-span-2',
31 30 labelWidth: 120,
32   - componentProps: { class: 'w-full' },
33 31 },
34 32 schema: [
35 33 {
... ... @@ -40,23 +38,23 @@ const [BasicForm, formApi] = useVbenForm({
40 38 },
41 39 {
42 40 component: 'Input',
43   - fieldName: 'tenantConnectionString',
44   - label: '自定义连接串',
45   - formItemClass: 'col-span-2',
46 41 componentProps: {
47 42 placeholder: '可选;为空时后端按租户名称自动生成业务库连接串',
48 43 },
  44 + fieldName: 'tenantConnectionString',
  45 + formItemClass: 'col-span-2',
  46 + label: '自定义连接串',
49 47 },
50 48 {
51 49 component: 'Switch',
52   - fieldName: 'initializeDatabase',
53   - label: '初始化数据库',
54   - defaultValue: true,
55 50 componentProps: { class: 'w-fit' },
  51 + defaultValue: true,
56 52 dependencies: {
57 53 if: () => !isUpdate.value,
58 54 triggerFields: ['companyName'],
59 55 },
  56 + fieldName: 'initializeDatabase',
  57 + label: '初始化数据库',
60 58 },
61 59 ],
62 60 showDefaultActions: false,
... ... @@ -103,26 +101,29 @@ async function handleConfirm() {
103 101 return;
104 102 }
105 103 const values = cloneDeep(await formApi.getValues()) as Record<string, unknown>;
  104 + const name = String(values.companyName ?? '').trim();
  105 + const tenantConnectionString = String(
  106 + values.tenantConnectionString ?? '',
  107 + ).trim();
  108 +
106 109 if (isUpdate.value && recordId.value) {
107 110 await saasTenantUpdate({
108 111 dbType: dbType.value,
109 112 entityVersion: entityVersion.value,
110 113 id: recordId.value,
111   - name: String(values.companyName ?? '').trim(),
112   - tenantConnectionString: String(
113   - values.tenantConnectionString ?? '',
114   - ).trim(),
  114 + name,
  115 + tenantConnectionString,
115 116 });
116 117 } else {
117   - await thProvisionTenant({
  118 + const result = await thProvisionTenant({
118 119 dbType: 0,
119 120 initializeDatabase: values.initializeDatabase !== false,
120   - name: String(values.companyName ?? '').trim(),
121   - tenantConnectionString: String(
122   - values.tenantConnectionString ?? '',
123   - ).trim(),
  121 + name,
  122 + tenantConnectionString,
124 123 });
  124 + console.info('TH tenant provisioned', result);
125 125 }
  126 +
126 127 message.success($t('http.operationSuccess'));
127 128 emit('reload');
128 129 await handleCancel();
... ... @@ -142,7 +143,7 @@ async function handleCancel() {
142 143 <a-alert
143 144 v-if="!isUpdate"
144 145 class="mb-4"
145   - :message="$t('foodLabeling.platform.initialAdminSection')"
  146 + message="调用 /api/app/th-tenant-provisioning/provision,在主库登记租户并按需初始化独立业务库。"
146 147 show-icon
147 148 type="info"
148 149 />
... ...
美国版/Food Labeling Management App UniApp/src/utils/appAdminRole.ts
... ... @@ -4,10 +4,13 @@ import type { UsAppMyProfileOutputDto } from &#39;../services/usAppAuth&#39;
4 4 export function isAppAdminUser(profile: UsAppMyProfileOutputDto | null | undefined): boolean {
5 5 if (!profile) return false
6 6 const code = (profile.primaryRoleCode ?? '').trim().toLowerCase()
  7 + const compactCode = code.replace(/[\s_-]+/g, '')
7 8 if (code === 'admin') return true
  9 + if (compactCode === 'companyadmin') return true
8 10 const display = (profile.roleDisplay ?? '').trim().toLowerCase()
9 11 if (!display) return false
10 12 if (display.includes('administrator')) return true
11 13 if (display.includes('super admin')) return true
  14 + if (display.includes('company admin')) return true
12 15 return false
13 16 }
... ...
美国版/Food Labeling Management Platform/src/components/labels/LabelCategoriesView.tsx
... ... @@ -143,6 +143,8 @@ function locationsForCategoryAvailability(c: LabelCategoryDto, catalog: Location
143 143 }
144 144  
145 145 function categoryRegionSummary(c: LabelCategoryDto, catalog: LocationDto[]): string {
  146 + const apiRegion = (c.region ?? "").trim();
  147 + if (apiRegion) return apiRegion;
146 148 const at = String(c.availabilityType ?? "ALL").trim().toUpperCase();
147 149 if (at !== "SPECIFIED") return "All";
148 150 const matched = locationsForCategoryAvailability(c, catalog);
... ... @@ -152,6 +154,8 @@ function categoryRegionSummary(c: LabelCategoryDto, catalog: LocationDto[]): str
152 154 }
153 155  
154 156 function categoryLocationLines(c: LabelCategoryDto, catalog: LocationDto[]): string[] {
  157 + const apiLoc = (c.location ?? "").trim();
  158 + if (apiLoc) return apiLoc.split(/\s*;\s*/).map((x) => x.trim()).filter(Boolean);
155 159 const at = String(c.availabilityType ?? "ALL").trim().toUpperCase();
156 160 if (at !== "SPECIFIED") return ["All"];
157 161 const matched = locationsForCategoryAvailability(c, catalog);
... ...
美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/LabelCanvas.tsx
... ... @@ -372,7 +372,9 @@ export function clampLabelElementBox(
372 372 baseH: number,
373 373 marginPx: number = LABEL_CANVAS_SAFE_MARGIN_PX,
374 374 orientation: PrintOrientation = 'vertical',
  375 + snap: boolean = true,
375 376 ): { x: number; y: number; w: number; h: number } {
  377 + const align = snap ? snapToGrid : (value: number) => value;
376 378 if (orientation === 'horizontal') {
377 379 const cx = baseW / 2;
378 380 const cy = baseH / 2;
... ... @@ -384,21 +386,21 @@ export function clampLabelElementBox(
384 386 return {
385 387 x: minX,
386 388 y: minY,
387   - w: Math.max(GRID_SIZE, snapToGrid(innerW)),
388   - h: Math.max(GRID_SIZE, snapToGrid(innerH)),
  389 + w: Math.max(GRID_SIZE, align(innerW)),
  390 + h: Math.max(GRID_SIZE, align(innerH)),
389 391 };
390 392 }
391   - let cw = Math.min(Math.max(20, snapToGrid(w)), snapToGrid(innerW));
392   - let ch = Math.min(Math.max(12, snapToGrid(h)), snapToGrid(innerH));
  393 + let cw = Math.min(Math.max(20, align(w)), align(innerW));
  394 + let ch = Math.min(Math.max(12, align(h)), align(innerH));
393 395 const maxX = baseH - marginPx + cx - cy - cw;
394 396 const maxY = cx + cy - marginPx - ch;
395 397 if (maxX < minX || maxY < minY) {
396   - return { x: minX, y: minY, w: snapToGrid(innerW), h: snapToGrid(innerH) };
  398 + return { x: minX, y: minY, w: align(innerW), h: align(innerH) };
397 399 }
398   - let nx = snapToGrid(x);
399   - let ny = snapToGrid(y);
400   - nx = Math.min(Math.max(nx, minX), snapToGrid(maxX));
401   - ny = Math.min(Math.max(ny, minY), snapToGrid(maxY));
  400 + let nx = align(x);
  401 + let ny = align(y);
  402 + nx = Math.min(Math.max(nx, minX), align(maxX));
  403 + ny = Math.min(Math.max(ny, minY), align(maxY));
402 404 return { x: nx, y: ny, w: cw, h: ch };
403 405 }
404 406  
... ... @@ -406,22 +408,22 @@ export function clampLabelElementBox(
406 408 const minY = marginPx;
407 409 const maxR = baseW - marginPx;
408 410 const maxB = baseH - marginPx;
409   - let cw = Math.max(20, snapToGrid(w));
410   - let ch = Math.max(12, snapToGrid(h));
  411 + let cw = Math.max(20, align(w));
  412 + let ch = Math.max(12, align(h));
411 413 const innerW = maxR - minX;
412 414 const innerH = maxB - minY;
413 415 if (innerW < GRID_SIZE || innerH < GRID_SIZE) {
414 416 return {
415 417 x: minX,
416 418 y: minY,
417   - w: Math.max(GRID_SIZE, snapToGrid(innerW)),
418   - h: Math.max(GRID_SIZE, snapToGrid(innerH)),
  419 + w: Math.max(GRID_SIZE, align(innerW)),
  420 + h: Math.max(GRID_SIZE, align(innerH)),
419 421 };
420 422 }
421   - cw = Math.min(cw, snapToGrid(innerW));
422   - ch = Math.min(ch, snapToGrid(innerH));
423   - let cx = snapToGrid(x);
424   - let cy = snapToGrid(y);
  423 + cw = Math.min(cw, align(innerW));
  424 + ch = Math.min(ch, align(innerH));
  425 + let cx = align(x);
  426 + let cy = align(y);
425 427 cx = Math.min(Math.max(cx, minX), Math.max(minX, maxR - cw));
426 428 cy = Math.min(Math.max(cy, minY), Math.max(minY, maxB - ch));
427 429 return { x: cx, y: cy, w: cw, h: ch };
... ...
美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/PropertiesPanel.tsx
... ... @@ -14,7 +14,6 @@ import { Switch } from &#39;../../ui/switch&#39;;
14 14 import type {
15 15 LabelTemplate,
16 16 LabelElement,
17   - Unit,
18 17 Rotation,
19 18 Border,
20 19 NutritionExtraItem,
... ... @@ -53,12 +52,10 @@ import {
53 52 } from '../../../utils/textElementLayout';
54 53 import {
55 54 type PreviewRulerDisplayUnit,
56   - displayLengthToElementPx,
57   - elementPxToDisplayLength,
58 55 formatPreviewRulerDisplayValue,
59 56 previewRulerUnitLabel,
60 57 } from '../../../utils/previewRulerUnits';
61   -import { unitToPx } from '../../../utils/labelTemplateUnits';
  58 +import { pxToUnit, unitToPx } from '../../../utils/labelTemplateUnits';
62 59 import { ImageUrlUpload } from '../../ui/image-url-upload';
63 60 import { readImageScaleMode } from '../../../utils/imageScaleMode';
64 61 import {
... ... @@ -111,7 +108,7 @@ interface PropertiesPanelProps {
111 108 template: LabelTemplate;
112 109 selectedElement: LabelElement | null;
113 110 onTemplateChange: (patch: Partial<LabelTemplate>) => void;
114   - onElementChange: (id: string, patch: Partial<LabelElement>) => void;
  111 + onElementChange: (id: string, patch: Partial<LabelElement>, options?: { snapToGrid?: boolean }) => void;
115 112 onDeleteElement?: (id: string) => void;
116 113 /** 编辑已有模板时禁止修改 Template Code */
117 114 readOnlyTemplateCode?: boolean;
... ... @@ -186,27 +183,23 @@ export function PropertiesPanel({
186 183 <ElementDimensionField
187 184 label="Width"
188 185 pxValue={selectedElement.width}
189   - paperSizeTemplate={template.width}
190   - templateUnit={template.unit}
191 186 displayUnit={previewRulerUnit}
192 187 disabled={positionLocked}
193 188 onPxChange={(width) =>
194 189 onElementChange(selectedElement.id, {
195 190 width: Math.max(1, width),
196   - })
  191 + }, { snapToGrid: false })
197 192 }
198 193 />
199 194 <ElementDimensionField
200 195 label="Height"
201 196 pxValue={selectedElement.height}
202   - paperSizeTemplate={template.height}
203   - templateUnit={template.unit}
204 197 displayUnit={previewRulerUnit}
205 198 disabled={positionLocked}
206 199 onPxChange={(height) =>
207 200 onElementChange(selectedElement.id, {
208 201 height: Math.max(1, height),
209   - })
  202 + }, { snapToGrid: false })
210 203 }
211 204 />
212 205 </div>
... ... @@ -549,29 +542,17 @@ function CompanyPrintFieldsEditor({
549 542 function ElementDimensionField({
550 543 label,
551 544 pxValue,
552   - paperSizeTemplate,
553   - templateUnit,
554 545 displayUnit,
555 546 onPxChange,
556 547 disabled = false,
557 548 }: {
558 549 label: string;
559 550 pxValue: number;
560   - paperSizeTemplate: number;
561   - templateUnit: Unit;
562 551 displayUnit: PreviewRulerDisplayUnit;
563 552 onPxChange: (px: number) => void;
564 553 disabled?: boolean;
565 554 }) {
566   - const basePaperPx = unitToPx(Number(paperSizeTemplate) || 0, templateUnit);
567   - const paperSize = Number(paperSizeTemplate) || 0;
568   - const displayValue = elementPxToDisplayLength(
569   - pxValue,
570   - basePaperPx,
571   - paperSize,
572   - templateUnit,
573   - displayUnit,
574   - );
  555 + const displayValue = pxToUnit(pxValue, displayUnit);
575 556 const formatted = formatPreviewRulerDisplayValue(displayValue, displayUnit);
576 557 const unitLabel = previewRulerUnitLabel(displayUnit);
577 558 const step = displayUnit === 'mm' ? 1 : displayUnit === 'inch' ? 0.0001 : 0.001;
... ... @@ -585,19 +566,11 @@ function ElementDimensionField({
585 566 if (trimmed === '' || trimmed === '-' || trimmed === '.') return false;
586 567 const nextDisplay = Number(trimmed);
587 568 if (!Number.isFinite(nextDisplay)) return false;
588   - const nextPx = Math.round(
589   - displayLengthToElementPx(
590   - nextDisplay,
591   - basePaperPx,
592   - paperSize,
593   - templateUnit,
594   - displayUnit,
595   - ),
596   - );
  569 + const nextPx = unitToPx(nextDisplay, displayUnit);
597 570 onPxChange(Math.max(1, nextPx));
598 571 return true;
599 572 },
600   - [basePaperPx, displayUnit, onPxChange, paperSize, templateUnit],
  573 + [displayUnit, onPxChange],
601 574 );
602 575  
603 576 return (
... ... @@ -618,17 +591,7 @@ function ElementDimensionField({
618 591 setEditing(false);
619 592 }}
620 593 onChange={(e) => {
621   - const raw = e.target.value;
622   - setDraft(raw);
623   - const trimmed = raw.trim();
624   - if (
625   - trimmed !== '' &&
626   - trimmed !== '-' &&
627   - !trimmed.endsWith('.') &&
628   - !trimmed.endsWith('-')
629   - ) {
630   - commitDisplay(raw);
631   - }
  594 + setDraft(e.target.value);
632 595 }}
633 596 onKeyDown={(e) => {
634 597 if (e.key === 'Enter') {
... ... @@ -1038,6 +1001,7 @@ function ElementConfigFields({
1038 1001 onChange={(e) => update('data', e.target.value)}
1039 1002 className="h-8 text-sm mt-1"
1040 1003 />
  1004 + <p className="text-[10px] text-gray-400 mt-1">{DEMO_VALUE_HINT}</p>
1041 1005 </div>
1042 1006 <div className="grid grid-cols-2 gap-2">
1043 1007 <div>
... ... @@ -1089,6 +1053,7 @@ function ElementConfigFields({
1089 1053 onChange={(e) => update('data', e.target.value)}
1090 1054 className="h-8 text-sm mt-1"
1091 1055 />
  1056 + <p className="text-[10px] text-gray-400 mt-1">{DEMO_VALUE_HINT}</p>
1092 1057 </div>
1093 1058 );
1094 1059 case 'IMAGE': {
... ...
美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/index.tsx
... ... @@ -401,7 +401,7 @@ export function LabelTemplateEditor({
401 401 /** 配置区单行 flex */
402 402 const configRowClass = "flex w-full min-w-0 flex-wrap items-center gap-2";
403 403  
404   - const updateElement = useCallback((id: string, patch: Partial<LabelElement>) => {
  404 + const updateElement = useCallback((id: string, patch: Partial<LabelElement>, options?: { snapToGrid?: boolean }) => {
405 405 const geomTouched =
406 406 patch.x !== undefined ||
407 407 patch.y !== undefined ||
... ... @@ -451,6 +451,7 @@ export function LabelTemplateEditor({
451 451 baseH,
452 452 undefined,
453 453 orientation,
  454 + options?.snapToGrid ?? true,
454 455 );
455 456 return {
456 457 ...merged,
... ...
美国版/Food Labeling Management Platform/src/components/locations/LocationsView.tsx
... ... @@ -139,6 +139,7 @@ export type LocationsViewProps = {
139 139 };
140 140  
141 141 export function LocationsView({ renderBeforeTabs, canMutateLocations = true }: LocationsViewProps = {}) {
  142 + const scopeAuth = useCategoryScopeAuth();
142 143 const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
143 144 const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
144 145 const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
... ... @@ -164,6 +165,7 @@ export function LocationsView({ renderBeforeTabs, canMutateLocations = true }: L
164 165 /** 筛选栏:公司 / 区域选项来自接口,而非当前门店分页结果去重 */
165 166 const [filterPartnerOptions, setFilterPartnerOptions] = useState<PartnerListItem[]>([]);
166 167 const [filterGroupOptions, setFilterGroupOptions] = useState<GroupListItem[]>([]);
  168 + const [filterLocationOptions, setFilterLocationOptions] = useState<LocationDto[]>([]);
167 169  
168 170 const [pageIndex, setPageIndex] = useState(1);
169 171 const [pageSize, setPageSize] = useState(10);
... ... @@ -194,6 +196,15 @@ export function LocationsView({ renderBeforeTabs, canMutateLocations = true }: L
194 196 return ["all", ...names];
195 197 }, [filterPartnerOptions]);
196 198  
  199 + const fixedPartnerName = useMemo(() => {
  200 + if (scopeAuth.requireCompanySelection) return "";
  201 + const fixedPartnerId = scopeAuth.fixedPartnerId.trim();
  202 + if (!fixedPartnerId) return "";
  203 + return (
  204 + filterPartnerOptions.find((p) => p.id === fixedPartnerId)?.partnerName?.trim() ?? ""
  205 + );
  206 + }, [scopeAuth.requireCompanySelection, scopeAuth.fixedPartnerId, filterPartnerOptions]);
  207 +
197 208 const groupOptions = useMemo(() => {
198 209 const names = filterGroupOptions
199 210 .map((g) => (g.groupName ?? "").trim())
... ... @@ -204,12 +215,12 @@ export function LocationsView({ renderBeforeTabs, canMutateLocations = true }: L
204 215  
205 216 const locationOptions = useMemo(() => {
206 217 const s = new Set<string>();
207   - for (const x of locations) {
208   - const v = (x.locationCode ?? "").trim();
  218 + for (const x of filterLocationOptions) {
  219 + const v = (x.locationName ?? "").trim();
209 220 if (v) s.add(v);
210 221 }
211 222 return ["all", ...Array.from(s).sort((a, b) => a.localeCompare(b))];
212   - }, [locations]);
  223 + }, [filterLocationOptions]);
213 224  
214 225 const totalPages = Math.max(1, Math.ceil(total / pageSize));
215 226  
... ... @@ -227,6 +238,12 @@ export function LocationsView({ renderBeforeTabs, canMutateLocations = true }: L
227 238 }, []);
228 239  
229 240 useEffect(() => {
  241 + if (scopeAuth.requireCompanySelection) return;
  242 + if (!fixedPartnerName || partner === fixedPartnerName) return;
  243 + setPartner(fixedPartnerName);
  244 + }, [scopeAuth.requireCompanySelection, fixedPartnerName, partner]);
  245 +
  246 + useEffect(() => {
230 247 const ac = new AbortController();
231 248 (async () => {
232 249 try {
... ... @@ -271,6 +288,38 @@ export function LocationsView({ renderBeforeTabs, canMutateLocations = true }: L
271 288 }, [groupName, groupOptions]);
272 289  
273 290 useEffect(() => {
  291 + const ac = new AbortController();
  292 + (async () => {
  293 + try {
  294 + const out: LocationDto[] = [];
  295 + const size = 100;
  296 + for (let page = 1; page <= 200; page += 1) {
  297 + const res = await getLocations(
  298 + {
  299 + skipCount: page,
  300 + maxResultCount: size,
  301 + partner: partner !== "all" ? partner : undefined,
  302 + groupName: groupName !== "all" ? groupName : undefined,
  303 + },
  304 + ac.signal,
  305 + );
  306 + out.push(...(res.items ?? []));
  307 + if (!res.items || res.items.length < size) break;
  308 + }
  309 + setFilterLocationOptions(out);
  310 + } catch {
  311 + if (!ac.signal.aborted) setFilterLocationOptions([]);
  312 + }
  313 + })();
  314 + return () => ac.abort();
  315 + }, [partner, groupName, refreshSeq]);
  316 +
  317 + useEffect(() => {
  318 + if (locationPick === "all") return;
  319 + if (!locationOptions.includes(locationPick)) setLocationPick("all");
  320 + }, [locationPick, locationOptions]);
  321 +
  322 + useEffect(() => {
274 323 // When filter changes, reset to first page.
275 324 setPageIndex(1);
276 325 // eslint-disable-next-line react-hooks/exhaustive-deps
... ... @@ -351,18 +400,20 @@ export function LocationsView({ renderBeforeTabs, canMutateLocations = true }: L
351 400 style={{ height: 40, boxSizing: "border-box" }}
352 401 className="border border-gray-300 rounded-md w-40 shrink-0 bg-white placeholder:text-gray-500"
353 402 />
354   - <Select value={partner} onValueChange={setPartner}>
355   - <SelectTrigger className="w-[140px] h-10 rounded-md border border-gray-300 bg-white font-medium text-gray-900 shrink-0" style={{ height: 40, boxSizing: "border-box" }}>
356   - <SelectValue placeholder="Company" />
357   - </SelectTrigger>
358   - <SelectContent>
359   - {partnerOptions.map((p) => (
360   - <SelectItem key={p} value={p}>
361   - {p === "all" ? "Company (All)" : p}
362   - </SelectItem>
363   - ))}
364   - </SelectContent>
365   - </Select>
  403 + {scopeAuth.requireCompanySelection ? (
  404 + <Select value={partner} onValueChange={setPartner}>
  405 + <SelectTrigger className="w-[140px] h-10 rounded-md border border-gray-300 bg-white font-medium text-gray-900 shrink-0" style={{ height: 40, boxSizing: "border-box" }}>
  406 + <SelectValue placeholder="Company" />
  407 + </SelectTrigger>
  408 + <SelectContent>
  409 + {partnerOptions.map((p) => (
  410 + <SelectItem key={p} value={p}>
  411 + {p === "all" ? "Company (All)" : p}
  412 + </SelectItem>
  413 + ))}
  414 + </SelectContent>
  415 + </Select>
  416 + ) : null}
366 417 <Select value={groupName} onValueChange={setGroupName}>
367 418 <SelectTrigger className="w-[140px] h-10 rounded-md border border-gray-300 bg-white font-medium text-gray-900 shrink-0" style={{ height: 40, boxSizing: "border-box" }}>
368 419 <SelectValue placeholder="Region" />
... ...
美国版/Food Labeling Management Platform/src/components/people/PeopleView.tsx
... ... @@ -187,13 +187,6 @@ function normLocationId(id: string | null | undefined): string {
187 187 return String(id ?? "").trim().toLowerCase();
188 188 }
189 189  
190   -function memberMatchesLocationScope(m: TeamMemberDto, allowedLocationIds: Set<string> | null): boolean {
191   - if (allowedLocationIds === null) return true;
192   - if (allowedLocationIds.size === 0) return false;
193   - const ids = (m.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean);
194   - return ids.some((id) => allowedLocationIds.has(id));
195   -}
196   -
197 190 // --- Types ---
198 191 type ViewTab = "Roles" | "Partner" | "Group" | "Location Manager" | "Team Member";
199 192  
... ... @@ -247,6 +240,7 @@ export function PeopleView({
247 240 onProfileEditIntentConsumed,
248 241 }: PeopleViewProps = {}) {
249 242 const auth = useAuth();
  243 + const scopeAuth = useCategoryScopeAuth();
250 244 const canAmStructureMutate = useMemo(
251 245 () =>
252 246 canMutateAccountManagementStructure({
... ... @@ -564,22 +558,6 @@ export function PeopleView({
564 558 [memberFilterLocations, memberSelectedPartner, memberSelectedGroup],
565 559 );
566 560  
567   - const memberUsesClientScopeFilter = useMemo(
568   - () =>
569   - memberLocationFilter === "all" &&
570   - (memberPartnerFilter !== "all" || memberGroupFilter !== "all"),
571   - [memberPartnerFilter, memberGroupFilter, memberLocationFilter],
572   - );
573   -
574   - const memberAllowedLocationIds = useMemo((): Set<string> | null => {
575   - if (memberLocationFilter !== "all" || !memberUsesClientScopeFilter) return null;
576   - const ids = new Set<string>();
577   - for (const loc of memberLocationsForSelect) {
578   - if (loc.id) ids.add(loc.id);
579   - }
580   - return ids;
581   - }, [memberLocationFilter, memberUsesClientScopeFilter, memberLocationsForSelect]);
582   -
583 561 useEffect(() => {
584 562 if (activeTab !== "Team Member") return;
585 563 const ac = new AbortController();
... ... @@ -603,9 +581,10 @@ export function PeopleView({
603 581 }, [activeTab, memberRefreshSeq]);
604 582  
605 583 useEffect(() => {
  584 + if (!scopeAuth.requireCompanySelection) return;
606 585 if (memberPartnerFilter === "all") return;
607 586 if (!memberCompanyOptions.some((p) => p.id === memberPartnerFilter)) setMemberPartnerFilter("all");
608   - }, [memberPartnerFilter, memberCompanyOptions]);
  587 + }, [scopeAuth.requireCompanySelection, memberPartnerFilter, memberCompanyOptions]);
609 588  
610 589 useEffect(() => {
611 590 if (memberGroupFilter === "all") return;
... ... @@ -626,6 +605,19 @@ export function PeopleView({
626 605 }, [debouncedGroupKeyword, groupPageSize, groupPartnerFilter, groupStatusFilter]);
627 606  
628 607 useEffect(() => {
  608 + if (scopeAuth.requireCompanySelection) return;
  609 + const fixed = scopeAuth.fixedPartnerId.trim();
  610 + if (!fixed) return;
  611 + if (groupPartnerFilter !== fixed) setGroupPartnerFilter(fixed);
  612 + if (memberPartnerFilter !== fixed) setMemberPartnerFilter(fixed);
  613 + }, [
  614 + scopeAuth.requireCompanySelection,
  615 + scopeAuth.fixedPartnerId,
  616 + groupPartnerFilter,
  617 + memberPartnerFilter,
  618 + ]);
  619 +
  620 + useEffect(() => {
629 621 if (activeTab !== "Group") return;
630 622 const ac = new AbortController();
631 623 (async () => {
... ... @@ -763,38 +755,25 @@ export function PeopleView({
763 755  
764 756 setMembersLoading(true);
765 757 try {
  758 + const partnerIdParam =
  759 + memberPartnerFilter !== "all" ? memberPartnerFilter : undefined;
  760 + const groupIdParam =
  761 + memberGroupFilter !== "all" ? memberGroupFilter : undefined;
766 762 const locationIdParam =
767 763 memberLocationFilter !== "all" ? memberLocationFilter : undefined;
768   -
769   - if (memberUsesClientScopeFilter) {
770   - const res = await getTeamMembers(
771   - {
772   - skipCount: 1,
773   - maxResultCount: 500,
774   - keyword: debouncedMemberKeyword || undefined,
775   - },
776   - ac.signal,
777   - );
778   - const filtered = (res.items ?? []).filter((m) =>
779   - memberMatchesLocationScope(m, memberAllowedLocationIds),
780   - );
781   - const total = filtered.length;
782   - const start = (memberPageIndex - 1) * memberPageSize;
783   - setMembers(filtered.slice(start, start + memberPageSize));
784   - setMemberTotal(total);
785   - } else {
786   - const res = await getTeamMembers(
787   - {
788   - skipCount: Math.max(1, memberPageIndex),
789   - maxResultCount: memberPageSize,
790   - keyword: debouncedMemberKeyword || undefined,
791   - locationId: locationIdParam,
792   - },
793   - ac.signal,
794   - );
795   - setMembers(res.items ?? []);
796   - setMemberTotal(res.totalCount ?? 0);
797   - }
  764 + const res = await getTeamMembers(
  765 + {
  766 + skipCount: Math.max(1, memberPageIndex),
  767 + maxResultCount: memberPageSize,
  768 + keyword: debouncedMemberKeyword || undefined,
  769 + partnerId: partnerIdParam,
  770 + groupId: groupIdParam,
  771 + locationId: locationIdParam,
  772 + },
  773 + ac.signal,
  774 + );
  775 + setMembers(res.items ?? []);
  776 + setMemberTotal(res.totalCount ?? 0);
798 777 } catch (e: any) {
799 778 if (e?.name === "AbortError") return;
800 779 toast.error("Failed to load team members.", {
... ... @@ -815,9 +794,9 @@ export function PeopleView({
815 794 memberPageIndex,
816 795 memberPageSize,
817 796 memberRefreshSeq,
  797 + memberPartnerFilter,
  798 + memberGroupFilter,
818 799 memberLocationFilter,
819   - memberUsesClientScopeFilter,
820   - memberAllowedLocationIds,
821 800 ]);
822 801  
823 802 const openCreateDialog = () => {
... ... @@ -914,19 +893,21 @@ export function PeopleView({
914 893 )}
915 894 {activeTab === "Team Member" && (
916 895 <>
917   - <Select value={memberPartnerFilter} onValueChange={setMemberPartnerFilter}>
918   - <SelectTrigger className="w-[160px] h-10 rounded-md border border-gray-300 bg-white font-medium text-gray-900 shrink-0">
919   - <SelectValue placeholder="Company" />
920   - </SelectTrigger>
921   - <SelectContent>
922   - <SelectItem value="all">All Companies</SelectItem>
923   - {memberCompanyOptions.map((p) => (
924   - <SelectItem key={p.id} value={p.id}>
925   - {(p.partnerName ?? "").trim() || p.id}
926   - </SelectItem>
927   - ))}
928   - </SelectContent>
929   - </Select>
  896 + {scopeAuth.requireCompanySelection ? (
  897 + <Select value={memberPartnerFilter} onValueChange={setMemberPartnerFilter}>
  898 + <SelectTrigger className="w-[160px] h-10 rounded-md border border-gray-300 bg-white font-medium text-gray-900 shrink-0">
  899 + <SelectValue placeholder="Company" />
  900 + </SelectTrigger>
  901 + <SelectContent>
  902 + <SelectItem value="all">All Companies</SelectItem>
  903 + {memberCompanyOptions.map((p) => (
  904 + <SelectItem key={p.id} value={p.id}>
  905 + {(p.partnerName ?? "").trim() || p.id}
  906 + </SelectItem>
  907 + ))}
  908 + </SelectContent>
  909 + </Select>
  910 + ) : null}
930 911 <Select value={memberGroupFilter} onValueChange={setMemberGroupFilter}>
931 912 <SelectTrigger className="w-[160px] h-10 rounded-md border border-gray-300 bg-white font-medium text-gray-900 shrink-0">
932 913 <SelectValue placeholder="Region" />
... ... @@ -957,19 +938,21 @@ export function PeopleView({
957 938 )}
958 939 {activeTab === "Group" && (
959 940 <>
960   - <Select value={groupPartnerFilter} onValueChange={setGroupPartnerFilter}>
961   - <SelectTrigger className="w-[180px] h-10 rounded-md border border-gray-300 bg-white font-medium text-gray-900 shrink-0">
962   - <SelectValue placeholder="Parent company" />
963   - </SelectTrigger>
964   - <SelectContent>
965   - <SelectItem value="all">Company (All)</SelectItem>
966   - {groupFilterPartnerOptions.map((p) => (
967   - <SelectItem key={p.id} value={p.id}>
968   - {p.partnerName ?? p.id}
969   - </SelectItem>
970   - ))}
971   - </SelectContent>
972   - </Select>
  941 + {scopeAuth.requireCompanySelection ? (
  942 + <Select value={groupPartnerFilter} onValueChange={setGroupPartnerFilter}>
  943 + <SelectTrigger className="w-[180px] h-10 rounded-md border border-gray-300 bg-white font-medium text-gray-900 shrink-0">
  944 + <SelectValue placeholder="Parent company" />
  945 + </SelectTrigger>
  946 + <SelectContent>
  947 + <SelectItem value="all">Company (All)</SelectItem>
  948 + {groupFilterPartnerOptions.map((p) => (
  949 + <SelectItem key={p.id} value={p.id}>
  950 + {p.partnerName ?? p.id}
  951 + </SelectItem>
  952 + ))}
  953 + </SelectContent>
  954 + </Select>
  955 + ) : null}
973 956 <Select
974 957 value={groupStatusFilter}
975 958 onValueChange={(v) => setGroupStatusFilter(v as "all" | "active" | "inactive")}
... ...
美国版/Food Labeling Management Platform/src/components/products/ProductsView.tsx
... ... @@ -286,6 +286,7 @@ async function loadAllLocationsForCatalog(signal?: AbortSignal): Promise&lt;Locatio
286 286 }
287 287  
288 288 export function ProductsView() {
  289 + const scopeAuth = useCategoryScopeAuth();
289 290 const [activeTab, setActiveTab] = useState<"products" | "categories">("products");
290 291 const [products, setProducts] = useState<ProductDto[]>([]);
291 292 const [total, setTotal] = useState(0);
... ... @@ -383,6 +384,13 @@ export function ProductsView() {
383 384 }, [debouncedKeyword, companyFilter, regionFilter, locationFilter, categoryFilter, stateFilter, pageSize]);
384 385  
385 386 useEffect(() => {
  387 + if (scopeAuth.requireCompanySelection) return;
  388 + const fixed = scopeAuth.fixedPartnerId.trim();
  389 + if (!fixed || companyFilter === fixed) return;
  390 + setCompanyFilter(fixed);
  391 + }, [scopeAuth.requireCompanySelection, scopeAuth.fixedPartnerId, companyFilter]);
  392 +
  393 + useEffect(() => {
386 394 setCatPageIndex(1);
387 395 }, [debouncedKeyword, stateFilter, catPageSize, companyFilter, regionFilter, locationFilter]);
388 396  
... ... @@ -449,9 +457,10 @@ export function ProductsView() {
449 457 }, [regionFilter]);
450 458  
451 459 useEffect(() => {
  460 + if (!scopeAuth.requireCompanySelection) return;
452 461 if (companyFilter === "all") return;
453 462 if (!companyOptions.some((p) => p.id === companyFilter)) setCompanyFilter("all");
454   - }, [companyFilter, companyOptions]);
  463 + }, [scopeAuth.requireCompanySelection, companyFilter, companyOptions]);
455 464  
456 465 useEffect(() => {
457 466 if (regionFilter === "all") return;
... ... @@ -694,22 +703,24 @@ export function ProductsView() {
694 703 className="flex-1 min-w-0 border-0 bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 py-2 px-2 h-full placeholder:text-gray-500"
695 704 />
696 705 </div>
697   - <Select value={companyFilter} onValueChange={setCompanyFilter}>
698   - <SelectTrigger
699   - className="w-[140px] h-10 rounded-md border border-gray-300 bg-white font-medium text-gray-900 shrink-0"
700   - style={{ height: 40, boxSizing: "border-box" }}
701   - >
702   - <SelectValue placeholder="Company" />
703   - </SelectTrigger>
704   - <SelectContent>
705   - <SelectItem value="all">All Companies</SelectItem>
706   - {companyOptions.map((c) => (
707   - <SelectItem key={c.id} value={c.id}>
708   - {toDisplay(c.partnerName)}
709   - </SelectItem>
710   - ))}
711   - </SelectContent>
712   - </Select>
  706 + {scopeAuth.requireCompanySelection ? (
  707 + <Select value={companyFilter} onValueChange={setCompanyFilter}>
  708 + <SelectTrigger
  709 + className="w-[140px] h-10 rounded-md border border-gray-300 bg-white font-medium text-gray-900 shrink-0"
  710 + style={{ height: 40, boxSizing: "border-box" }}
  711 + >
  712 + <SelectValue placeholder="Company" />
  713 + </SelectTrigger>
  714 + <SelectContent>
  715 + <SelectItem value="all">All Companies</SelectItem>
  716 + {companyOptions.map((c) => (
  717 + <SelectItem key={c.id} value={c.id}>
  718 + {toDisplay(c.partnerName)}
  719 + </SelectItem>
  720 + ))}
  721 + </SelectContent>
  722 + </Select>
  723 + ) : null}
713 724 <Select value={regionFilter} onValueChange={setRegionFilter}>
714 725 <SelectTrigger
715 726 className="w-[140px] h-10 rounded-md border border-gray-300 bg-white font-medium text-gray-900 shrink-0"
... ...
美国版/Food Labeling Management Platform/src/types/labelCategory.ts
... ... @@ -31,6 +31,8 @@ export type LabelCategoryDto = {
31 31 locationIds?: string[] | null;
32 32 /** 列表:Company 展示 */
33 33 company?: string | null;
  34 + region?: string | null;
  35 + location?: string | null;
34 36 /** 最近编辑时间(列表接口映射 LastModificationTime ?? CreationTime) */
35 37 lastEdited?: string | null;
36 38 };
... ...