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,6 +15,10 @@ export interface ThProvisionTenantOutputDto {
15 databaseInitialized: boolean; 15 databaseInitialized: boolean;
16 } 16 }
17 17
  18 +export interface ThInitializeTenantDatabaseInput {
  19 + tenantId: string;
  20 +}
  21 +
18 function normalizeProvisionTenantOutput( 22 function normalizeProvisionTenantOutput(
19 raw: unknown, 23 raw: unknown,
20 ): ThProvisionTenantOutputDto { 24 ): ThProvisionTenantOutputDto {
@@ -50,3 +54,19 @@ export async function thProvisionTenant( @@ -50,3 +54,19 @@ export async function thProvisionTenant(
50 ); 54 );
51 return normalizeProvisionTenantOutput(raw); 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,8 +18,9 @@ let loginInFlight: Promise&lt;ThWebLoginOutputDto&gt; | null = null;
18 function normalizeWebLoginOutput(raw: unknown): ThWebLoginOutputDto { 18 function normalizeWebLoginOutput(raw: unknown): ThWebLoginOutputDto {
19 const o = 19 const o =
20 raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : {}; 20 raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : {};
  21 + const token = String(o.token ?? o.Token ?? '').trim();
21 return { 22 return {
22 - token: String(o.token ?? o.Token ?? ''), 23 + token: token.replace(/^Bearer\s+/i, ''),
23 refreshToken: String(o.refreshToken ?? o.RefreshToken ?? ''), 24 refreshToken: String(o.refreshToken ?? o.RefreshToken ?? ''),
24 tenantId: String(o.tenantId ?? o.TenantId ?? ''), 25 tenantId: String(o.tenantId ?? o.TenantId ?? ''),
25 tenantName: String(o.tenantName ?? o.TenantName ?? ''), 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,6 +34,7 @@ const captchaInfo = ref&lt;CaptchaResponse&gt;({
34 const captchaLoading = ref(false); 34 const captchaLoading = ref(false);
35 const tenantOptions = ref<ThTenantSelectDto[]>([]); 35 const tenantOptions = ref<ThTenantSelectDto[]>([]);
36 const loginSubmitting = ref(false); 36 const loginSubmitting = ref(false);
  37 +const LAST_TENANT_KEY = 'th:last-tenant-id';
37 38
38 async function loadCaptcha() { 39 async function loadCaptcha() {
39 try { 40 try {
@@ -54,9 +55,13 @@ async function loadTenant() { @@ -54,9 +55,13 @@ async function loadTenant() {
54 try { 55 try {
55 const list = await thTenantSelectList(); 56 const list = await thTenantSelectList();
56 tenantOptions.value = list; 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 } catch (error) { 65 } catch (error) {
61 console.error(error); 66 console.error(error);
62 tenantOptions.value = []; 67 tenantOptions.value = [];
@@ -153,6 +158,7 @@ async function handleAccountLogin(values: LoginAndRegisterParams) { @@ -153,6 +158,7 @@ async function handleAccountLogin(values: LoginAndRegisterParams) {
153 ? { code: String(values.code ?? ''), uuid: captchaInfo.value.uuid } 158 ? { code: String(values.code ?? ''), uuid: captchaInfo.value.uuid }
154 : {}), 159 : {}),
155 }); 160 });
  161 + localStorage.setItem(LAST_TENANT_KEY, String(values.tenantId ?? ''));
156 } catch (error) { 162 } catch (error) {
157 console.error(error); 163 console.error(error);
158 if (error instanceof Error && captchaInfo.value.isEnableCaptcha) { 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,27 +9,27 @@ export const querySchema: FormSchemaGetter = () =&gt; [...keywordFilterSchema()];
9 export const columns: VxeGridProps['columns'] = [ 9 export const columns: VxeGridProps['columns'] = [
10 { 10 {
11 field: 'companyName', 11 field: 'companyName',
12 - title: '租户名称',  
13 minWidth: 180, 12 minWidth: 180,
14 slots: { default: 'companyName' }, 13 slots: { default: 'companyName' },
  14 + title: '租户名称',
15 }, 15 },
16 { 16 {
17 field: 'companyCode', 17 field: 'companyCode',
18 - title: '租户 ID',  
19 minWidth: 260, 18 minWidth: 260,
20 slots: { default: 'tenantId' }, 19 slots: { default: 'tenantId' },
  20 + title: '租户 ID',
21 }, 21 },
22 { 22 {
23 field: 'remark', 23 field: 'remark',
24 - title: '连接串 / 数据库',  
25 minWidth: 240, 24 minWidth: 240,
26 slots: { default: 'connectionString' }, 25 slots: { default: 'connectionString' },
  26 + title: '连接串 / 数据库',
27 }, 27 },
28 { 28 {
29 field: 'creationTime', 29 field: 'creationTime',
30 - title: '创建时间',  
31 minWidth: 160, 30 minWidth: 160,
32 slots: { default: 'creationTime' }, 31 slots: { default: 'creationTime' },
  32 + title: '创建时间',
33 }, 33 },
34 foodLabelingActionColumn({ minWidth: 120 }), 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,7 +84,7 @@ async function handleDelete(row: SaasTenantDto) {
84 function confirmDelete(row: SaasTenantDto) { 84 function confirmDelete(row: SaasTenantDto) {
85 Modal.confirm({ 85 Modal.confirm({
86 okType: 'danger', 86 okType: 'danger',
87 - title: $t('foodLabeling.platform.deleteCompanyConfirm'), 87 + title: '确认删除该租户?',
88 onOk: () => handleDelete(row), 88 onOk: () => handleDelete(row),
89 }); 89 });
90 } 90 }
@@ -94,23 +94,25 @@ function confirmDelete(row: SaasTenantDto) { @@ -94,23 +94,25 @@ function confirmDelete(row: SaasTenantDto) {
94 <Page :auto-content-height="true" :content-class="managementTabPageContentClass"> 94 <Page :auto-content-height="true" :content-class="managementTabPageContentClass">
95 <Alert 95 <Alert
96 class="mb-3 shrink-0" 96 class="mb-3 shrink-0"
97 - message="平台管理员:此处通过 tenant-select 查看租户,通过 provision 开通独立库租户;业务接口依赖登录 Token 或 __tenant 请求头识别租户上下文。" 97 + message="平台管理员:此处通过 tenant-select 查看租户,通过 provision 开通独立库租户;业务接口依赖登录 Token 或 __tenant 请求头识别租户上下文。"
98 show-icon 98 show-icon
99 type="info" 99 type="info"
100 /> 100 />
101 101
102 <BasicTable :class="managementTabTableClass"> 102 <BasicTable :class="managementTabTableClass">
103 <template #toolbar-tools> 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 </template> 105 </template>
108 106
109 <template #companyName="{ row }"> 107 <template #companyName="{ row }">
110 <div class="flex min-w-0 items-center gap-2"> 108 <div class="flex min-w-0 items-center gap-2">
111 <Avatar v-if="row.logoUrl" :src="row.logoUrl" size="small" /> 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 </div> 116 </div>
115 </template> 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,16 +20,14 @@ const entityVersion = ref&lt;number | undefined&gt;();
20 const dbType = ref<number | undefined>(); 20 const dbType = ref<number | undefined>();
21 21
22 const title = computed(() => 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 const [BasicForm, formApi] = useVbenForm({ 26 const [BasicForm, formApi] = useVbenForm({
29 commonConfig: { 27 commonConfig: {
  28 + componentProps: { class: 'w-full' },
30 formItemClass: 'col-span-2', 29 formItemClass: 'col-span-2',
31 labelWidth: 120, 30 labelWidth: 120,
32 - componentProps: { class: 'w-full' },  
33 }, 31 },
34 schema: [ 32 schema: [
35 { 33 {
@@ -40,23 +38,23 @@ const [BasicForm, formApi] = useVbenForm({ @@ -40,23 +38,23 @@ const [BasicForm, formApi] = useVbenForm({
40 }, 38 },
41 { 39 {
42 component: 'Input', 40 component: 'Input',
43 - fieldName: 'tenantConnectionString',  
44 - label: '自定义连接串',  
45 - formItemClass: 'col-span-2',  
46 componentProps: { 41 componentProps: {
47 placeholder: '可选;为空时后端按租户名称自动生成业务库连接串', 42 placeholder: '可选;为空时后端按租户名称自动生成业务库连接串',
48 }, 43 },
  44 + fieldName: 'tenantConnectionString',
  45 + formItemClass: 'col-span-2',
  46 + label: '自定义连接串',
49 }, 47 },
50 { 48 {
51 component: 'Switch', 49 component: 'Switch',
52 - fieldName: 'initializeDatabase',  
53 - label: '初始化数据库',  
54 - defaultValue: true,  
55 componentProps: { class: 'w-fit' }, 50 componentProps: { class: 'w-fit' },
  51 + defaultValue: true,
56 dependencies: { 52 dependencies: {
57 if: () => !isUpdate.value, 53 if: () => !isUpdate.value,
58 triggerFields: ['companyName'], 54 triggerFields: ['companyName'],
59 }, 55 },
  56 + fieldName: 'initializeDatabase',
  57 + label: '初始化数据库',
60 }, 58 },
61 ], 59 ],
62 showDefaultActions: false, 60 showDefaultActions: false,
@@ -103,26 +101,29 @@ async function handleConfirm() { @@ -103,26 +101,29 @@ async function handleConfirm() {
103 return; 101 return;
104 } 102 }
105 const values = cloneDeep(await formApi.getValues()) as Record<string, unknown>; 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 if (isUpdate.value && recordId.value) { 109 if (isUpdate.value && recordId.value) {
107 await saasTenantUpdate({ 110 await saasTenantUpdate({
108 dbType: dbType.value, 111 dbType: dbType.value,
109 entityVersion: entityVersion.value, 112 entityVersion: entityVersion.value,
110 id: recordId.value, 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 } else { 117 } else {
117 - await thProvisionTenant({ 118 + const result = await thProvisionTenant({
118 dbType: 0, 119 dbType: 0,
119 initializeDatabase: values.initializeDatabase !== false, 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 message.success($t('http.operationSuccess')); 127 message.success($t('http.operationSuccess'));
127 emit('reload'); 128 emit('reload');
128 await handleCancel(); 129 await handleCancel();
@@ -142,7 +143,7 @@ async function handleCancel() { @@ -142,7 +143,7 @@ async function handleCancel() {
142 <a-alert 143 <a-alert
143 v-if="!isUpdate" 144 v-if="!isUpdate"
144 class="mb-4" 145 class="mb-4"
145 - :message="$t('foodLabeling.platform.initialAdminSection')" 146 + message="调用 /api/app/th-tenant-provisioning/provision,在主库登记租户并按需初始化独立业务库。"
146 show-icon 147 show-icon
147 type="info" 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,10 +4,13 @@ import type { UsAppMyProfileOutputDto } from &#39;../services/usAppAuth&#39;
4 export function isAppAdminUser(profile: UsAppMyProfileOutputDto | null | undefined): boolean { 4 export function isAppAdminUser(profile: UsAppMyProfileOutputDto | null | undefined): boolean {
5 if (!profile) return false 5 if (!profile) return false
6 const code = (profile.primaryRoleCode ?? '').trim().toLowerCase() 6 const code = (profile.primaryRoleCode ?? '').trim().toLowerCase()
  7 + const compactCode = code.replace(/[\s_-]+/g, '')
7 if (code === 'admin') return true 8 if (code === 'admin') return true
  9 + if (compactCode === 'companyadmin') return true
8 const display = (profile.roleDisplay ?? '').trim().toLowerCase() 10 const display = (profile.roleDisplay ?? '').trim().toLowerCase()
9 if (!display) return false 11 if (!display) return false
10 if (display.includes('administrator')) return true 12 if (display.includes('administrator')) return true
11 if (display.includes('super admin')) return true 13 if (display.includes('super admin')) return true
  14 + if (display.includes('company admin')) return true
12 return false 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,6 +143,8 @@ function locationsForCategoryAvailability(c: LabelCategoryDto, catalog: Location
143 } 143 }
144 144
145 function categoryRegionSummary(c: LabelCategoryDto, catalog: LocationDto[]): string { 145 function categoryRegionSummary(c: LabelCategoryDto, catalog: LocationDto[]): string {
  146 + const apiRegion = (c.region ?? "").trim();
  147 + if (apiRegion) return apiRegion;
146 const at = String(c.availabilityType ?? "ALL").trim().toUpperCase(); 148 const at = String(c.availabilityType ?? "ALL").trim().toUpperCase();
147 if (at !== "SPECIFIED") return "All"; 149 if (at !== "SPECIFIED") return "All";
148 const matched = locationsForCategoryAvailability(c, catalog); 150 const matched = locationsForCategoryAvailability(c, catalog);
@@ -152,6 +154,8 @@ function categoryRegionSummary(c: LabelCategoryDto, catalog: LocationDto[]): str @@ -152,6 +154,8 @@ function categoryRegionSummary(c: LabelCategoryDto, catalog: LocationDto[]): str
152 } 154 }
153 155
154 function categoryLocationLines(c: LabelCategoryDto, catalog: LocationDto[]): string[] { 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 const at = String(c.availabilityType ?? "ALL").trim().toUpperCase(); 159 const at = String(c.availabilityType ?? "ALL").trim().toUpperCase();
156 if (at !== "SPECIFIED") return ["All"]; 160 if (at !== "SPECIFIED") return ["All"];
157 const matched = locationsForCategoryAvailability(c, catalog); 161 const matched = locationsForCategoryAvailability(c, catalog);
美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/LabelCanvas.tsx
@@ -372,7 +372,9 @@ export function clampLabelElementBox( @@ -372,7 +372,9 @@ export function clampLabelElementBox(
372 baseH: number, 372 baseH: number,
373 marginPx: number = LABEL_CANVAS_SAFE_MARGIN_PX, 373 marginPx: number = LABEL_CANVAS_SAFE_MARGIN_PX,
374 orientation: PrintOrientation = 'vertical', 374 orientation: PrintOrientation = 'vertical',
  375 + snap: boolean = true,
375 ): { x: number; y: number; w: number; h: number } { 376 ): { x: number; y: number; w: number; h: number } {
  377 + const align = snap ? snapToGrid : (value: number) => value;
376 if (orientation === 'horizontal') { 378 if (orientation === 'horizontal') {
377 const cx = baseW / 2; 379 const cx = baseW / 2;
378 const cy = baseH / 2; 380 const cy = baseH / 2;
@@ -384,21 +386,21 @@ export function clampLabelElementBox( @@ -384,21 +386,21 @@ export function clampLabelElementBox(
384 return { 386 return {
385 x: minX, 387 x: minX,
386 y: minY, 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 const maxX = baseH - marginPx + cx - cy - cw; 395 const maxX = baseH - marginPx + cx - cy - cw;
394 const maxY = cx + cy - marginPx - ch; 396 const maxY = cx + cy - marginPx - ch;
395 if (maxX < minX || maxY < minY) { 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 return { x: nx, y: ny, w: cw, h: ch }; 404 return { x: nx, y: ny, w: cw, h: ch };
403 } 405 }
404 406
@@ -406,22 +408,22 @@ export function clampLabelElementBox( @@ -406,22 +408,22 @@ export function clampLabelElementBox(
406 const minY = marginPx; 408 const minY = marginPx;
407 const maxR = baseW - marginPx; 409 const maxR = baseW - marginPx;
408 const maxB = baseH - marginPx; 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 const innerW = maxR - minX; 413 const innerW = maxR - minX;
412 const innerH = maxB - minY; 414 const innerH = maxB - minY;
413 if (innerW < GRID_SIZE || innerH < GRID_SIZE) { 415 if (innerW < GRID_SIZE || innerH < GRID_SIZE) {
414 return { 416 return {
415 x: minX, 417 x: minX,
416 y: minY, 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 cx = Math.min(Math.max(cx, minX), Math.max(minX, maxR - cw)); 427 cx = Math.min(Math.max(cx, minX), Math.max(minX, maxR - cw));
426 cy = Math.min(Math.max(cy, minY), Math.max(minY, maxB - ch)); 428 cy = Math.min(Math.max(cy, minY), Math.max(minY, maxB - ch));
427 return { x: cx, y: cy, w: cw, h: ch }; 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,7 +14,6 @@ import { Switch } from &#39;../../ui/switch&#39;;
14 import type { 14 import type {
15 LabelTemplate, 15 LabelTemplate,
16 LabelElement, 16 LabelElement,
17 - Unit,  
18 Rotation, 17 Rotation,
19 Border, 18 Border,
20 NutritionExtraItem, 19 NutritionExtraItem,
@@ -53,12 +52,10 @@ import { @@ -53,12 +52,10 @@ import {
53 } from '../../../utils/textElementLayout'; 52 } from '../../../utils/textElementLayout';
54 import { 53 import {
55 type PreviewRulerDisplayUnit, 54 type PreviewRulerDisplayUnit,
56 - displayLengthToElementPx,  
57 - elementPxToDisplayLength,  
58 formatPreviewRulerDisplayValue, 55 formatPreviewRulerDisplayValue,
59 previewRulerUnitLabel, 56 previewRulerUnitLabel,
60 } from '../../../utils/previewRulerUnits'; 57 } from '../../../utils/previewRulerUnits';
61 -import { unitToPx } from '../../../utils/labelTemplateUnits'; 58 +import { pxToUnit, unitToPx } from '../../../utils/labelTemplateUnits';
62 import { ImageUrlUpload } from '../../ui/image-url-upload'; 59 import { ImageUrlUpload } from '../../ui/image-url-upload';
63 import { readImageScaleMode } from '../../../utils/imageScaleMode'; 60 import { readImageScaleMode } from '../../../utils/imageScaleMode';
64 import { 61 import {
@@ -111,7 +108,7 @@ interface PropertiesPanelProps { @@ -111,7 +108,7 @@ interface PropertiesPanelProps {
111 template: LabelTemplate; 108 template: LabelTemplate;
112 selectedElement: LabelElement | null; 109 selectedElement: LabelElement | null;
113 onTemplateChange: (patch: Partial<LabelTemplate>) => void; 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 onDeleteElement?: (id: string) => void; 112 onDeleteElement?: (id: string) => void;
116 /** 编辑已有模板时禁止修改 Template Code */ 113 /** 编辑已有模板时禁止修改 Template Code */
117 readOnlyTemplateCode?: boolean; 114 readOnlyTemplateCode?: boolean;
@@ -186,27 +183,23 @@ export function PropertiesPanel({ @@ -186,27 +183,23 @@ export function PropertiesPanel({
186 <ElementDimensionField 183 <ElementDimensionField
187 label="Width" 184 label="Width"
188 pxValue={selectedElement.width} 185 pxValue={selectedElement.width}
189 - paperSizeTemplate={template.width}  
190 - templateUnit={template.unit}  
191 displayUnit={previewRulerUnit} 186 displayUnit={previewRulerUnit}
192 disabled={positionLocked} 187 disabled={positionLocked}
193 onPxChange={(width) => 188 onPxChange={(width) =>
194 onElementChange(selectedElement.id, { 189 onElementChange(selectedElement.id, {
195 width: Math.max(1, width), 190 width: Math.max(1, width),
196 - }) 191 + }, { snapToGrid: false })
197 } 192 }
198 /> 193 />
199 <ElementDimensionField 194 <ElementDimensionField
200 label="Height" 195 label="Height"
201 pxValue={selectedElement.height} 196 pxValue={selectedElement.height}
202 - paperSizeTemplate={template.height}  
203 - templateUnit={template.unit}  
204 displayUnit={previewRulerUnit} 197 displayUnit={previewRulerUnit}
205 disabled={positionLocked} 198 disabled={positionLocked}
206 onPxChange={(height) => 199 onPxChange={(height) =>
207 onElementChange(selectedElement.id, { 200 onElementChange(selectedElement.id, {
208 height: Math.max(1, height), 201 height: Math.max(1, height),
209 - }) 202 + }, { snapToGrid: false })
210 } 203 }
211 /> 204 />
212 </div> 205 </div>
@@ -549,29 +542,17 @@ function CompanyPrintFieldsEditor({ @@ -549,29 +542,17 @@ function CompanyPrintFieldsEditor({
549 function ElementDimensionField({ 542 function ElementDimensionField({
550 label, 543 label,
551 pxValue, 544 pxValue,
552 - paperSizeTemplate,  
553 - templateUnit,  
554 displayUnit, 545 displayUnit,
555 onPxChange, 546 onPxChange,
556 disabled = false, 547 disabled = false,
557 }: { 548 }: {
558 label: string; 549 label: string;
559 pxValue: number; 550 pxValue: number;
560 - paperSizeTemplate: number;  
561 - templateUnit: Unit;  
562 displayUnit: PreviewRulerDisplayUnit; 551 displayUnit: PreviewRulerDisplayUnit;
563 onPxChange: (px: number) => void; 552 onPxChange: (px: number) => void;
564 disabled?: boolean; 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 const formatted = formatPreviewRulerDisplayValue(displayValue, displayUnit); 556 const formatted = formatPreviewRulerDisplayValue(displayValue, displayUnit);
576 const unitLabel = previewRulerUnitLabel(displayUnit); 557 const unitLabel = previewRulerUnitLabel(displayUnit);
577 const step = displayUnit === 'mm' ? 1 : displayUnit === 'inch' ? 0.0001 : 0.001; 558 const step = displayUnit === 'mm' ? 1 : displayUnit === 'inch' ? 0.0001 : 0.001;
@@ -585,19 +566,11 @@ function ElementDimensionField({ @@ -585,19 +566,11 @@ function ElementDimensionField({
585 if (trimmed === '' || trimmed === '-' || trimmed === '.') return false; 566 if (trimmed === '' || trimmed === '-' || trimmed === '.') return false;
586 const nextDisplay = Number(trimmed); 567 const nextDisplay = Number(trimmed);
587 if (!Number.isFinite(nextDisplay)) return false; 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 onPxChange(Math.max(1, nextPx)); 570 onPxChange(Math.max(1, nextPx));
598 return true; 571 return true;
599 }, 572 },
600 - [basePaperPx, displayUnit, onPxChange, paperSize, templateUnit], 573 + [displayUnit, onPxChange],
601 ); 574 );
602 575
603 return ( 576 return (
@@ -618,17 +591,7 @@ function ElementDimensionField({ @@ -618,17 +591,7 @@ function ElementDimensionField({
618 setEditing(false); 591 setEditing(false);
619 }} 592 }}
620 onChange={(e) => { 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 onKeyDown={(e) => { 596 onKeyDown={(e) => {
634 if (e.key === 'Enter') { 597 if (e.key === 'Enter') {
@@ -1038,6 +1001,7 @@ function ElementConfigFields({ @@ -1038,6 +1001,7 @@ function ElementConfigFields({
1038 onChange={(e) => update('data', e.target.value)} 1001 onChange={(e) => update('data', e.target.value)}
1039 className="h-8 text-sm mt-1" 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 </div> 1005 </div>
1042 <div className="grid grid-cols-2 gap-2"> 1006 <div className="grid grid-cols-2 gap-2">
1043 <div> 1007 <div>
@@ -1089,6 +1053,7 @@ function ElementConfigFields({ @@ -1089,6 +1053,7 @@ function ElementConfigFields({
1089 onChange={(e) => update('data', e.target.value)} 1053 onChange={(e) => update('data', e.target.value)}
1090 className="h-8 text-sm mt-1" 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 </div> 1057 </div>
1093 ); 1058 );
1094 case 'IMAGE': { 1059 case 'IMAGE': {
美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/index.tsx
@@ -401,7 +401,7 @@ export function LabelTemplateEditor({ @@ -401,7 +401,7 @@ export function LabelTemplateEditor({
401 /** 配置区单行 flex */ 401 /** 配置区单行 flex */
402 const configRowClass = "flex w-full min-w-0 flex-wrap items-center gap-2"; 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 const geomTouched = 405 const geomTouched =
406 patch.x !== undefined || 406 patch.x !== undefined ||
407 patch.y !== undefined || 407 patch.y !== undefined ||
@@ -451,6 +451,7 @@ export function LabelTemplateEditor({ @@ -451,6 +451,7 @@ export function LabelTemplateEditor({
451 baseH, 451 baseH,
452 undefined, 452 undefined,
453 orientation, 453 orientation,
  454 + options?.snapToGrid ?? true,
454 ); 455 );
455 return { 456 return {
456 ...merged, 457 ...merged,
美国版/Food Labeling Management Platform/src/components/locations/LocationsView.tsx
@@ -139,6 +139,7 @@ export type LocationsViewProps = { @@ -139,6 +139,7 @@ export type LocationsViewProps = {
139 }; 139 };
140 140
141 export function LocationsView({ renderBeforeTabs, canMutateLocations = true }: LocationsViewProps = {}) { 141 export function LocationsView({ renderBeforeTabs, canMutateLocations = true }: LocationsViewProps = {}) {
  142 + const scopeAuth = useCategoryScopeAuth();
142 const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); 143 const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
143 const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); 144 const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
144 const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); 145 const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
@@ -164,6 +165,7 @@ export function LocationsView({ renderBeforeTabs, canMutateLocations = true }: L @@ -164,6 +165,7 @@ export function LocationsView({ renderBeforeTabs, canMutateLocations = true }: L
164 /** 筛选栏:公司 / 区域选项来自接口,而非当前门店分页结果去重 */ 165 /** 筛选栏:公司 / 区域选项来自接口,而非当前门店分页结果去重 */
165 const [filterPartnerOptions, setFilterPartnerOptions] = useState<PartnerListItem[]>([]); 166 const [filterPartnerOptions, setFilterPartnerOptions] = useState<PartnerListItem[]>([]);
166 const [filterGroupOptions, setFilterGroupOptions] = useState<GroupListItem[]>([]); 167 const [filterGroupOptions, setFilterGroupOptions] = useState<GroupListItem[]>([]);
  168 + const [filterLocationOptions, setFilterLocationOptions] = useState<LocationDto[]>([]);
167 169
168 const [pageIndex, setPageIndex] = useState(1); 170 const [pageIndex, setPageIndex] = useState(1);
169 const [pageSize, setPageSize] = useState(10); 171 const [pageSize, setPageSize] = useState(10);
@@ -194,6 +196,15 @@ export function LocationsView({ renderBeforeTabs, canMutateLocations = true }: L @@ -194,6 +196,15 @@ export function LocationsView({ renderBeforeTabs, canMutateLocations = true }: L
194 return ["all", ...names]; 196 return ["all", ...names];
195 }, [filterPartnerOptions]); 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 const groupOptions = useMemo(() => { 208 const groupOptions = useMemo(() => {
198 const names = filterGroupOptions 209 const names = filterGroupOptions
199 .map((g) => (g.groupName ?? "").trim()) 210 .map((g) => (g.groupName ?? "").trim())
@@ -204,12 +215,12 @@ export function LocationsView({ renderBeforeTabs, canMutateLocations = true }: L @@ -204,12 +215,12 @@ export function LocationsView({ renderBeforeTabs, canMutateLocations = true }: L
204 215
205 const locationOptions = useMemo(() => { 216 const locationOptions = useMemo(() => {
206 const s = new Set<string>(); 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 if (v) s.add(v); 220 if (v) s.add(v);
210 } 221 }
211 return ["all", ...Array.from(s).sort((a, b) => a.localeCompare(b))]; 222 return ["all", ...Array.from(s).sort((a, b) => a.localeCompare(b))];
212 - }, [locations]); 223 + }, [filterLocationOptions]);
213 224
214 const totalPages = Math.max(1, Math.ceil(total / pageSize)); 225 const totalPages = Math.max(1, Math.ceil(total / pageSize));
215 226
@@ -227,6 +238,12 @@ export function LocationsView({ renderBeforeTabs, canMutateLocations = true }: L @@ -227,6 +238,12 @@ export function LocationsView({ renderBeforeTabs, canMutateLocations = true }: L
227 }, []); 238 }, []);
228 239
229 useEffect(() => { 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 const ac = new AbortController(); 247 const ac = new AbortController();
231 (async () => { 248 (async () => {
232 try { 249 try {
@@ -271,6 +288,38 @@ export function LocationsView({ renderBeforeTabs, canMutateLocations = true }: L @@ -271,6 +288,38 @@ export function LocationsView({ renderBeforeTabs, canMutateLocations = true }: L
271 }, [groupName, groupOptions]); 288 }, [groupName, groupOptions]);
272 289
273 useEffect(() => { 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 // When filter changes, reset to first page. 323 // When filter changes, reset to first page.
275 setPageIndex(1); 324 setPageIndex(1);
276 // eslint-disable-next-line react-hooks/exhaustive-deps 325 // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -351,18 +400,20 @@ export function LocationsView({ renderBeforeTabs, canMutateLocations = true }: L @@ -351,18 +400,20 @@ export function LocationsView({ renderBeforeTabs, canMutateLocations = true }: L
351 style={{ height: 40, boxSizing: "border-box" }} 400 style={{ height: 40, boxSizing: "border-box" }}
352 className="border border-gray-300 rounded-md w-40 shrink-0 bg-white placeholder:text-gray-500" 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 <Select value={groupName} onValueChange={setGroupName}> 417 <Select value={groupName} onValueChange={setGroupName}>
367 <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" }}> 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 <SelectValue placeholder="Region" /> 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,13 +187,6 @@ function normLocationId(id: string | null | undefined): string {
187 return String(id ?? "").trim().toLowerCase(); 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 // --- Types --- 190 // --- Types ---
198 type ViewTab = "Roles" | "Partner" | "Group" | "Location Manager" | "Team Member"; 191 type ViewTab = "Roles" | "Partner" | "Group" | "Location Manager" | "Team Member";
199 192
@@ -247,6 +240,7 @@ export function PeopleView({ @@ -247,6 +240,7 @@ export function PeopleView({
247 onProfileEditIntentConsumed, 240 onProfileEditIntentConsumed,
248 }: PeopleViewProps = {}) { 241 }: PeopleViewProps = {}) {
249 const auth = useAuth(); 242 const auth = useAuth();
  243 + const scopeAuth = useCategoryScopeAuth();
250 const canAmStructureMutate = useMemo( 244 const canAmStructureMutate = useMemo(
251 () => 245 () =>
252 canMutateAccountManagementStructure({ 246 canMutateAccountManagementStructure({
@@ -564,22 +558,6 @@ export function PeopleView({ @@ -564,22 +558,6 @@ export function PeopleView({
564 [memberFilterLocations, memberSelectedPartner, memberSelectedGroup], 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 useEffect(() => { 561 useEffect(() => {
584 if (activeTab !== "Team Member") return; 562 if (activeTab !== "Team Member") return;
585 const ac = new AbortController(); 563 const ac = new AbortController();
@@ -603,9 +581,10 @@ export function PeopleView({ @@ -603,9 +581,10 @@ export function PeopleView({
603 }, [activeTab, memberRefreshSeq]); 581 }, [activeTab, memberRefreshSeq]);
604 582
605 useEffect(() => { 583 useEffect(() => {
  584 + if (!scopeAuth.requireCompanySelection) return;
606 if (memberPartnerFilter === "all") return; 585 if (memberPartnerFilter === "all") return;
607 if (!memberCompanyOptions.some((p) => p.id === memberPartnerFilter)) setMemberPartnerFilter("all"); 586 if (!memberCompanyOptions.some((p) => p.id === memberPartnerFilter)) setMemberPartnerFilter("all");
608 - }, [memberPartnerFilter, memberCompanyOptions]); 587 + }, [scopeAuth.requireCompanySelection, memberPartnerFilter, memberCompanyOptions]);
609 588
610 useEffect(() => { 589 useEffect(() => {
611 if (memberGroupFilter === "all") return; 590 if (memberGroupFilter === "all") return;
@@ -626,6 +605,19 @@ export function PeopleView({ @@ -626,6 +605,19 @@ export function PeopleView({
626 }, [debouncedGroupKeyword, groupPageSize, groupPartnerFilter, groupStatusFilter]); 605 }, [debouncedGroupKeyword, groupPageSize, groupPartnerFilter, groupStatusFilter]);
627 606
628 useEffect(() => { 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 if (activeTab !== "Group") return; 621 if (activeTab !== "Group") return;
630 const ac = new AbortController(); 622 const ac = new AbortController();
631 (async () => { 623 (async () => {
@@ -763,38 +755,25 @@ export function PeopleView({ @@ -763,38 +755,25 @@ export function PeopleView({
763 755
764 setMembersLoading(true); 756 setMembersLoading(true);
765 try { 757 try {
  758 + const partnerIdParam =
  759 + memberPartnerFilter !== "all" ? memberPartnerFilter : undefined;
  760 + const groupIdParam =
  761 + memberGroupFilter !== "all" ? memberGroupFilter : undefined;
766 const locationIdParam = 762 const locationIdParam =
767 memberLocationFilter !== "all" ? memberLocationFilter : undefined; 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 } catch (e: any) { 777 } catch (e: any) {
799 if (e?.name === "AbortError") return; 778 if (e?.name === "AbortError") return;
800 toast.error("Failed to load team members.", { 779 toast.error("Failed to load team members.", {
@@ -815,9 +794,9 @@ export function PeopleView({ @@ -815,9 +794,9 @@ export function PeopleView({
815 memberPageIndex, 794 memberPageIndex,
816 memberPageSize, 795 memberPageSize,
817 memberRefreshSeq, 796 memberRefreshSeq,
  797 + memberPartnerFilter,
  798 + memberGroupFilter,
818 memberLocationFilter, 799 memberLocationFilter,
819 - memberUsesClientScopeFilter,  
820 - memberAllowedLocationIds,  
821 ]); 800 ]);
822 801
823 const openCreateDialog = () => { 802 const openCreateDialog = () => {
@@ -914,19 +893,21 @@ export function PeopleView({ @@ -914,19 +893,21 @@ export function PeopleView({
914 )} 893 )}
915 {activeTab === "Team Member" && ( 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 <Select value={memberGroupFilter} onValueChange={setMemberGroupFilter}> 911 <Select value={memberGroupFilter} onValueChange={setMemberGroupFilter}>
931 <SelectTrigger className="w-[160px] h-10 rounded-md border border-gray-300 bg-white font-medium text-gray-900 shrink-0"> 912 <SelectTrigger className="w-[160px] h-10 rounded-md border border-gray-300 bg-white font-medium text-gray-900 shrink-0">
932 <SelectValue placeholder="Region" /> 913 <SelectValue placeholder="Region" />
@@ -957,19 +938,21 @@ export function PeopleView({ @@ -957,19 +938,21 @@ export function PeopleView({
957 )} 938 )}
958 {activeTab === "Group" && ( 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 <Select 956 <Select
974 value={groupStatusFilter} 957 value={groupStatusFilter}
975 onValueChange={(v) => setGroupStatusFilter(v as "all" | "active" | "inactive")} 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,6 +286,7 @@ async function loadAllLocationsForCatalog(signal?: AbortSignal): Promise&lt;Locatio
286 } 286 }
287 287
288 export function ProductsView() { 288 export function ProductsView() {
  289 + const scopeAuth = useCategoryScopeAuth();
289 const [activeTab, setActiveTab] = useState<"products" | "categories">("products"); 290 const [activeTab, setActiveTab] = useState<"products" | "categories">("products");
290 const [products, setProducts] = useState<ProductDto[]>([]); 291 const [products, setProducts] = useState<ProductDto[]>([]);
291 const [total, setTotal] = useState(0); 292 const [total, setTotal] = useState(0);
@@ -383,6 +384,13 @@ export function ProductsView() { @@ -383,6 +384,13 @@ export function ProductsView() {
383 }, [debouncedKeyword, companyFilter, regionFilter, locationFilter, categoryFilter, stateFilter, pageSize]); 384 }, [debouncedKeyword, companyFilter, regionFilter, locationFilter, categoryFilter, stateFilter, pageSize]);
384 385
385 useEffect(() => { 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 setCatPageIndex(1); 394 setCatPageIndex(1);
387 }, [debouncedKeyword, stateFilter, catPageSize, companyFilter, regionFilter, locationFilter]); 395 }, [debouncedKeyword, stateFilter, catPageSize, companyFilter, regionFilter, locationFilter]);
388 396
@@ -449,9 +457,10 @@ export function ProductsView() { @@ -449,9 +457,10 @@ export function ProductsView() {
449 }, [regionFilter]); 457 }, [regionFilter]);
450 458
451 useEffect(() => { 459 useEffect(() => {
  460 + if (!scopeAuth.requireCompanySelection) return;
452 if (companyFilter === "all") return; 461 if (companyFilter === "all") return;
453 if (!companyOptions.some((p) => p.id === companyFilter)) setCompanyFilter("all"); 462 if (!companyOptions.some((p) => p.id === companyFilter)) setCompanyFilter("all");
454 - }, [companyFilter, companyOptions]); 463 + }, [scopeAuth.requireCompanySelection, companyFilter, companyOptions]);
455 464
456 useEffect(() => { 465 useEffect(() => {
457 if (regionFilter === "all") return; 466 if (regionFilter === "all") return;
@@ -694,22 +703,24 @@ export function ProductsView() { @@ -694,22 +703,24 @@ export function ProductsView() {
694 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" 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 </div> 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 <Select value={regionFilter} onValueChange={setRegionFilter}> 724 <Select value={regionFilter} onValueChange={setRegionFilter}>
714 <SelectTrigger 725 <SelectTrigger
715 className="w-[140px] h-10 rounded-md border border-gray-300 bg-white font-medium text-gray-900 shrink-0" 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,6 +31,8 @@ export type LabelCategoryDto = {
31 locationIds?: string[] | null; 31 locationIds?: string[] | null;
32 /** 列表:Company 展示 */ 32 /** 列表:Company 展示 */
33 company?: string | null; 33 company?: string | null;
  34 + region?: string | null;
  35 + location?: string | null;
34 /** 最近编辑时间(列表接口映射 LastModificationTime ?? CreationTime) */ 36 /** 最近编辑时间(列表接口映射 LastModificationTime ?? CreationTime) */
35 lastEdited?: string | null; 37 lastEdited?: string | null;
36 }; 38 };