Commit 4950e3a0af6e6960f33c62929b94ffd6777b904f
1 parent
0234cbca
前端最新修改
Showing
15 changed files
with
365 additions
and
330 deletions
泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/.env.development
| ... | ... | @@ -13,7 +13,7 @@ VITE_INJECT_APP_LOADING=true |
| 13 | 13 | |
| 14 | 14 | # 本地开发:直连线上 API(不走 Vite 代理;VITE_GLOB_API_URL 为绝对地址时 proxy 不启用) |
| 15 | 15 | # 线上:http://saas-test.3ffoodsafety.com/api/app |
| 16 | -VITE_GLOB_API_URL=http://saas-test.3ffoodsafety.com/api/app | |
| 16 | +VITE_GLOB_API_URL=http://192.168.31.87:19002/api/app | |
| 17 | 17 | |
| 18 | 18 | # 全局加密开关(即开启了加解密功能才会生效 不是全部接口加密 需要和后端对应) |
| 19 | 19 | VITE_GLOB_ENABLE_ENCRYPT=false | ... | ... |
泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/api/th/index.ts
泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/api/th/th-tenant-provisioning.ts
0 → 100644
| 1 | +import { requestClient } from '#/api/request'; | |
| 2 | + | |
| 3 | +export interface ThProvisionTenantInput { | |
| 4 | + name: string; | |
| 5 | + tenantConnectionString?: string; | |
| 6 | + dbType?: number; | |
| 7 | + initializeDatabase?: boolean; | |
| 8 | +} | |
| 9 | + | |
| 10 | +export interface ThProvisionTenantOutputDto { | |
| 11 | + tenantId: string; | |
| 12 | + name: string; | |
| 13 | + databaseName: string; | |
| 14 | + tenantConnectionString: string; | |
| 15 | + databaseInitialized: boolean; | |
| 16 | +} | |
| 17 | + | |
| 18 | +function normalizeProvisionTenantOutput( | |
| 19 | + raw: unknown, | |
| 20 | +): ThProvisionTenantOutputDto { | |
| 21 | + const o = | |
| 22 | + raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : {}; | |
| 23 | + return { | |
| 24 | + tenantId: String(o.tenantId ?? o.TenantId ?? ''), | |
| 25 | + name: String(o.name ?? o.Name ?? ''), | |
| 26 | + databaseName: String(o.databaseName ?? o.DatabaseName ?? ''), | |
| 27 | + tenantConnectionString: String( | |
| 28 | + o.tenantConnectionString ?? o.TenantConnectionString ?? '', | |
| 29 | + ), | |
| 30 | + databaseInitialized: | |
| 31 | + o.databaseInitialized === true || o.DatabaseInitialized === true, | |
| 32 | + }; | |
| 33 | +} | |
| 34 | + | |
| 35 | +export async function thProvisionTenant( | |
| 36 | + input: ThProvisionTenantInput, | |
| 37 | +): Promise<ThProvisionTenantOutputDto> { | |
| 38 | + const raw = await requestClient.post<unknown>( | |
| 39 | + 'th-tenant-provisioning/provision', | |
| 40 | + { | |
| 41 | + dbType: input.dbType ?? 0, | |
| 42 | + initializeDatabase: input.initializeDatabase ?? true, | |
| 43 | + name: input.name.trim(), | |
| 44 | + tenantConnectionString: | |
| 45 | + input.tenantConnectionString?.trim() || undefined, | |
| 46 | + }, | |
| 47 | + { | |
| 48 | + successMessageMode: 'message', | |
| 49 | + }, | |
| 50 | + ); | |
| 51 | + return normalizeProvisionTenantOutput(raw); | |
| 52 | +} | ... | ... |
泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/api/th/th-web-auth.ts
0 → 100644
| 1 | +export interface ThWebLoginInput { | |
| 2 | + tenantId: string; | |
| 3 | + userName: string; | |
| 4 | + password: string; | |
| 5 | + uuid?: string; | |
| 6 | + code?: string; | |
| 7 | +} | |
| 8 | + | |
| 9 | +export interface ThWebLoginOutputDto { | |
| 10 | + token: string; | |
| 11 | + refreshToken: string; | |
| 12 | + tenantId: string; | |
| 13 | + tenantName: string; | |
| 14 | +} | |
| 15 | + | |
| 16 | +let loginInFlight: Promise<ThWebLoginOutputDto> | null = null; | |
| 17 | + | |
| 18 | +function normalizeWebLoginOutput(raw: unknown): ThWebLoginOutputDto { | |
| 19 | + const o = | |
| 20 | + raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : {}; | |
| 21 | + return { | |
| 22 | + token: String(o.token ?? o.Token ?? ''), | |
| 23 | + refreshToken: String(o.refreshToken ?? o.RefreshToken ?? ''), | |
| 24 | + tenantId: String(o.tenantId ?? o.TenantId ?? ''), | |
| 25 | + tenantName: String(o.tenantName ?? o.TenantName ?? ''), | |
| 26 | + }; | |
| 27 | +} | |
| 28 | + | |
| 29 | +export async function thWebLogin( | |
| 30 | + input: ThWebLoginInput, | |
| 31 | +): Promise<ThWebLoginOutputDto> { | |
| 32 | + if (loginInFlight) { | |
| 33 | + return loginInFlight; | |
| 34 | + } | |
| 35 | + | |
| 36 | + loginInFlight = (async () => { | |
| 37 | + const { requestClient } = await import('#/api/request'); | |
| 38 | + const raw = await requestClient.post<unknown>( | |
| 39 | + 'th-web-auth/login', | |
| 40 | + { | |
| 41 | + tenantId: input.tenantId, | |
| 42 | + userName: input.userName.trim(), | |
| 43 | + password: input.password, | |
| 44 | + ...(input.uuid ? { uuid: input.uuid } : {}), | |
| 45 | + ...(input.code != null && input.code !== '' | |
| 46 | + ? { code: input.code } | |
| 47 | + : {}), | |
| 48 | + }, | |
| 49 | + { | |
| 50 | + errorMessageMode: 'none', | |
| 51 | + headers: { | |
| 52 | + __tenant: input.tenantId, | |
| 53 | + }, | |
| 54 | + successMessageMode: 'none', | |
| 55 | + }, | |
| 56 | + ); | |
| 57 | + return normalizeWebLoginOutput(raw); | |
| 58 | + })().finally(() => { | |
| 59 | + loginInFlight = null; | |
| 60 | + }); | |
| 61 | + | |
| 62 | + return loginInFlight; | |
| 63 | +} | ... | ... |
泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/store/auth.ts
| ... | ... | @@ -10,16 +10,16 @@ import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores'; |
| 10 | 10 | import { notification } from 'ant-design-vue'; |
| 11 | 11 | import { defineStore } from 'pinia'; |
| 12 | 12 | |
| 13 | -import { thAppLogin } from '#/api/th'; | |
| 13 | +import { thWebLogin } from '#/api/th'; | |
| 14 | 14 | import { $t } from '#/locales'; |
| 15 | 15 | |
| 16 | 16 | import { useDictStore } from './dict'; |
| 17 | 17 | import { useThTenantStore } from './th-tenant'; |
| 18 | 18 | |
| 19 | -/** 泰额多租户登录入参(POST /api/app/th-app-auth/login) */ | |
| 19 | +/** 泰额 Web 多租户登录入参(POST /api/app/th-web-auth/login) */ | |
| 20 | 20 | export interface ThAuthLoginParams { |
| 21 | 21 | tenantId: string; |
| 22 | - email: string; | |
| 22 | + userName: string; | |
| 23 | 23 | password: string; |
| 24 | 24 | uuid?: string; |
| 25 | 25 | code?: string; |
| ... | ... | @@ -33,16 +33,16 @@ export const useAuthStore = defineStore('auth', () => { |
| 33 | 33 | |
| 34 | 34 | const loginLoading = ref(false); |
| 35 | 35 | |
| 36 | - function buildUserInfoFromLogin(email: string, tenantName: string): UserInfo { | |
| 37 | - const normalizedEmail = email.trim(); | |
| 36 | + function buildUserInfoFromLogin(userName: string, tenantName: string): UserInfo { | |
| 37 | + const normalizedUserName = userName.trim(); | |
| 38 | 38 | return { |
| 39 | 39 | avatar: '', |
| 40 | - email: normalizedEmail, | |
| 40 | + email: normalizedUserName.includes('@') ? normalizedUserName : '', | |
| 41 | 41 | permissions: [], |
| 42 | - realName: tenantName || normalizedEmail, | |
| 42 | + realName: tenantName || normalizedUserName, | |
| 43 | 43 | roles: [], |
| 44 | - userId: normalizedEmail, | |
| 45 | - username: normalizedEmail, | |
| 44 | + userId: normalizedUserName, | |
| 45 | + username: normalizedUserName, | |
| 46 | 46 | homePath: preferences.app.defaultHomePath, |
| 47 | 47 | }; |
| 48 | 48 | } |
| ... | ... | @@ -58,9 +58,9 @@ export const useAuthStore = defineStore('auth', () => { |
| 58 | 58 | try { |
| 59 | 59 | loginLoading.value = true; |
| 60 | 60 | |
| 61 | - const result = await thAppLogin({ | |
| 61 | + const result = await thWebLogin({ | |
| 62 | 62 | tenantId: params.tenantId, |
| 63 | - email: params.email, | |
| 63 | + userName: params.userName, | |
| 64 | 64 | password: params.password, |
| 65 | 65 | uuid: params.uuid, |
| 66 | 66 | code: params.code, |
| ... | ... | @@ -78,10 +78,10 @@ export const useAuthStore = defineStore('auth', () => { |
| 78 | 78 | thTenantStore.setTenantContext({ |
| 79 | 79 | tenantId: result.tenantId, |
| 80 | 80 | tenantName: result.tenantName, |
| 81 | - locations: result.locations, | |
| 81 | + locations: [], | |
| 82 | 82 | }); |
| 83 | 83 | |
| 84 | - userInfo = buildUserInfoFromLogin(params.email, result.tenantName); | |
| 84 | + userInfo = buildUserInfoFromLogin(params.userName, result.tenantName); | |
| 85 | 85 | userStore.setUserInfo(userInfo); |
| 86 | 86 | |
| 87 | 87 | if (accessStore.loginExpired) { |
| ... | ... | @@ -138,10 +138,10 @@ export const useAuthStore = defineStore('auth', () => { |
| 138 | 138 | return cached; |
| 139 | 139 | } |
| 140 | 140 | |
| 141 | - // 泰额 SAAS:用户信息来自 th-app-auth/login,不走 Yi 框架 /account | |
| 141 | + // 泰额 SAAS:用户信息来自 th-web-auth/login,不走 Yi 框架 /account | |
| 142 | 142 | if (thTenantStore.tenantId && accessStore.accessToken) { |
| 143 | 143 | const fallback = buildUserInfoFromLogin( |
| 144 | - cached?.email || cached?.username || 'user', | |
| 144 | + cached?.username || cached?.email || 'user', | |
| 145 | 145 | thTenantStore.tenantName || '', |
| 146 | 146 | ); |
| 147 | 147 | userStore.setUserInfo(fallback); | ... | ... |
泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/views/_core/authentication/login.vue
| 1 | 1 | <script lang="ts" setup> |
| 2 | -import type { VbenFormSchema } from '@vben/common-ui'; | |
| 2 | +import type { | |
| 3 | + LoginAndRegisterParams, | |
| 4 | + VbenFormSchema, | |
| 5 | +} from '@vben/common-ui'; | |
| 3 | 6 | |
| 4 | 7 | import type { CaptchaResponse } from '#/api/core/captcha'; |
| 5 | 8 | import type { ThTenantSelectDto } from '#/api/th'; |
| ... | ... | @@ -98,12 +101,11 @@ const formSchema = computed((): VbenFormSchema[] => { |
| 98 | 101 | class: 'focus:border-primary', |
| 99 | 102 | placeholder: 'admin@example.com', |
| 100 | 103 | }, |
| 101 | - fieldName: 'email', | |
| 104 | + fieldName: 'username', | |
| 102 | 105 | label: 'Email', |
| 103 | 106 | rules: z |
| 104 | 107 | .string() |
| 105 | - .min(1, { message: 'Email is required.' }) | |
| 106 | - .email({ message: 'Invalid email format.' }), | |
| 108 | + .min(1, { message: 'Email is required.' }), | |
| 107 | 109 | }, |
| 108 | 110 | { |
| 109 | 111 | component: 'VbenInputPassword', |
| ... | ... | @@ -137,18 +139,18 @@ const formSchema = computed((): VbenFormSchema[] => { |
| 137 | 139 | ]; |
| 138 | 140 | }); |
| 139 | 141 | |
| 140 | -async function handleAccountLogin(values: Record<string, string>) { | |
| 142 | +async function handleAccountLogin(values: LoginAndRegisterParams) { | |
| 141 | 143 | if (loginSubmitting.value || authStore.loginLoading) { |
| 142 | 144 | return; |
| 143 | 145 | } |
| 144 | 146 | loginSubmitting.value = true; |
| 145 | 147 | try { |
| 146 | 148 | await authStore.authLogin({ |
| 147 | - tenantId: values.tenantId, | |
| 148 | - email: values.email, | |
| 149 | - password: values.password, | |
| 149 | + tenantId: String(values.tenantId ?? ''), | |
| 150 | + userName: String(values.username ?? ''), | |
| 151 | + password: String(values.password ?? ''), | |
| 150 | 152 | ...(captchaInfo.value.isEnableCaptcha |
| 151 | - ? { code: values.code, uuid: captchaInfo.value.uuid } | |
| 153 | + ? { code: String(values.code ?? ''), uuid: captchaInfo.value.uuid } | |
| 152 | 154 | : {}), |
| 153 | 155 | }); |
| 154 | 156 | } catch (error) { | ... | ... |
泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/views/food-labeling/platform/tenants/api.ts
0 → 100644
| 1 | +import type { PageResult } from '#/api/common'; | |
| 2 | +import type { Tenant } from '#/api/system/tenant/model'; | |
| 3 | + | |
| 4 | +import type { SaasTenantDto } from '../../shared/saas-types'; | |
| 5 | + | |
| 6 | +import { thTenantSelectList } from '#/api/th'; | |
| 7 | +import { tenantInfo, tenantList, tenantRemove, tenantUpdate } from '#/api/system/tenant'; | |
| 8 | + | |
| 9 | +export interface SaasTenantQuery { | |
| 10 | + Keyword?: string; | |
| 11 | + MaxResultCount?: number; | |
| 12 | + SkipCount?: number; | |
| 13 | + State?: boolean; | |
| 14 | +} | |
| 15 | + | |
| 16 | +export interface SaasTenantUpdateInput { | |
| 17 | + id: string; | |
| 18 | + name: string; | |
| 19 | + tenantConnectionString?: string; | |
| 20 | + dbType?: number; | |
| 21 | + entityVersion?: number; | |
| 22 | +} | |
| 23 | + | |
| 24 | +function normalizeTenant(raw: Tenant): SaasTenantDto { | |
| 25 | + const name = raw.name || raw.companyName || raw.tenantId || raw.id; | |
| 26 | + return { | |
| 27 | + id: raw.id, | |
| 28 | + companyName: name, | |
| 29 | + companyCode: raw.id, | |
| 30 | + logoUrl: null, | |
| 31 | + contactName: null, | |
| 32 | + contactEmail: null, | |
| 33 | + contactPhone: null, | |
| 34 | + address: null, | |
| 35 | + remark: raw.tenantConnectionString || null, | |
| 36 | + state: raw.status === false || raw.status === 'false' ? false : true, | |
| 37 | + menuPermissionKeys: [], | |
| 38 | + adminUserId: null, | |
| 39 | + adminUserName: null, | |
| 40 | + adminFullName: null, | |
| 41 | + adminEmail: null, | |
| 42 | + creationTime: raw.creationTime ?? null, | |
| 43 | + }; | |
| 44 | +} | |
| 45 | + | |
| 46 | +export async function saasTenantList( | |
| 47 | + query: SaasTenantQuery, | |
| 48 | +): Promise<PageResult<SaasTenantDto>> { | |
| 49 | + if (query.State === false) { | |
| 50 | + return { items: [], totalCount: 0 }; | |
| 51 | + } | |
| 52 | + | |
| 53 | + const resp = (await tenantList({ | |
| 54 | + MaxResultCount: query.MaxResultCount, | |
| 55 | + Name: query.Keyword, | |
| 56 | + SkipCount: query.SkipCount, | |
| 57 | + })) as unknown as PageResult<Tenant>; | |
| 58 | + | |
| 59 | + return { | |
| 60 | + items: (resp.items ?? []).map(normalizeTenant), | |
| 61 | + totalCount: resp.totalCount ?? 0, | |
| 62 | + }; | |
| 63 | +} | |
| 64 | + | |
| 65 | +export async function saasTenantSelectList( | |
| 66 | + query: SaasTenantQuery, | |
| 67 | +): Promise<PageResult<SaasTenantDto>> { | |
| 68 | + if (query.State === false) { | |
| 69 | + return { items: [], totalCount: 0 }; | |
| 70 | + } | |
| 71 | + | |
| 72 | + const keyword = query.Keyword?.trim().toLowerCase(); | |
| 73 | + let rows = (await thTenantSelectList()).map((item) => | |
| 74 | + normalizeTenant({ | |
| 75 | + id: item.id, | |
| 76 | + name: item.name, | |
| 77 | + tenantConnectionString: '', | |
| 78 | + dbType: 0, | |
| 79 | + entityVersion: 0, | |
| 80 | + isDeleted: false, | |
| 81 | + creationTime: '', | |
| 82 | + creatorId: null, | |
| 83 | + lastModifierId: null, | |
| 84 | + lastModificationTime: null, | |
| 85 | + }), | |
| 86 | + ); | |
| 87 | + | |
| 88 | + if (keyword) { | |
| 89 | + rows = rows.filter( | |
| 90 | + (row) => | |
| 91 | + row.companyName.toLowerCase().includes(keyword) || | |
| 92 | + row.id.toLowerCase().includes(keyword), | |
| 93 | + ); | |
| 94 | + } | |
| 95 | + | |
| 96 | + const page = Math.max(query.SkipCount ?? 1, 1); | |
| 97 | + const size = Math.max(query.MaxResultCount ?? 10, 1); | |
| 98 | + const start = (page - 1) * size; | |
| 99 | + return { | |
| 100 | + items: rows.slice(start, start + size), | |
| 101 | + totalCount: rows.length, | |
| 102 | + }; | |
| 103 | +} | |
| 104 | + | |
| 105 | +export async function saasTenantInfo(id: string): Promise<Tenant> { | |
| 106 | + return tenantInfo(id); | |
| 107 | +} | |
| 108 | + | |
| 109 | +export async function saasTenantUpdate(input: SaasTenantUpdateInput) { | |
| 110 | + return tenantUpdate({ | |
| 111 | + dbType: input.dbType ?? 0, | |
| 112 | + entityVersion: input.entityVersion, | |
| 113 | + id: input.id, | |
| 114 | + name: input.name, | |
| 115 | + tenantConnectionString: input.tenantConnectionString ?? '', | |
| 116 | + }); | |
| 117 | +} | |
| 118 | + | |
| 119 | +export async function saasTenantRemove(id: string) { | |
| 120 | + return tenantRemove([id]); | |
| 121 | +} | ... | ... |
泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/views/food-labeling/platform/tenants/data.ts
| 1 | 1 | import type { FormSchemaGetter } from '#/adapter/form'; |
| 2 | 2 | import type { VxeGridProps } from '#/adapter/vxe-table'; |
| 3 | 3 | |
| 4 | -import { $t } from '@vben/locales'; | |
| 5 | - | |
| 6 | 4 | import { foodLabelingActionColumn } from '../../management/shared/management-grid'; |
| 7 | -import { keywordFilterSchema, stateFilterSchema } from '../../shared/form-schema'; | |
| 5 | +import { keywordFilterSchema } from '../../shared/form-schema'; | |
| 8 | 6 | |
| 9 | -export const querySchema: FormSchemaGetter = () => [ | |
| 10 | - ...keywordFilterSchema(), | |
| 11 | - ...stateFilterSchema(), | |
| 12 | -]; | |
| 7 | +export const querySchema: FormSchemaGetter = () => [...keywordFilterSchema()]; | |
| 13 | 8 | |
| 14 | 9 | export const columns: VxeGridProps['columns'] = [ |
| 15 | 10 | { |
| 16 | 11 | field: 'companyName', |
| 17 | - title: $t('foodLabeling.platform.companyName'), | |
| 18 | - minWidth: 160, | |
| 12 | + title: '租户名称', | |
| 13 | + minWidth: 180, | |
| 19 | 14 | slots: { default: 'companyName' }, |
| 20 | 15 | }, |
| 21 | 16 | { |
| 22 | 17 | field: 'companyCode', |
| 23 | - title: $t('foodLabeling.platform.companyCode'), | |
| 24 | - minWidth: 120, | |
| 25 | - slots: { default: 'companyCode' }, | |
| 18 | + title: '租户 ID', | |
| 19 | + minWidth: 260, | |
| 20 | + slots: { default: 'tenantId' }, | |
| 26 | 21 | }, |
| 27 | 22 | { |
| 28 | - field: 'adminUserName', | |
| 29 | - title: $t('foodLabeling.platform.tenantAdmin'), | |
| 30 | - minWidth: 130, | |
| 31 | - slots: { default: 'adminUserName' }, | |
| 23 | + field: 'remark', | |
| 24 | + title: '连接串 / 数据库', | |
| 25 | + minWidth: 240, | |
| 26 | + slots: { default: 'connectionString' }, | |
| 32 | 27 | }, |
| 33 | 28 | { |
| 34 | - field: 'menuPermissionKeys', | |
| 35 | - title: $t('foodLabeling.platform.menuPermissions'), | |
| 36 | - minWidth: 100, | |
| 37 | - slots: { default: 'menuCount' }, | |
| 38 | - }, | |
| 39 | - { | |
| 40 | - field: 'contactEmail', | |
| 41 | - title: $t('foodLabeling.accountManagement.contactEmail'), | |
| 29 | + field: 'creationTime', | |
| 30 | + title: '创建时间', | |
| 42 | 31 | minWidth: 160, |
| 43 | - slots: { default: 'contactEmail' }, | |
| 44 | - }, | |
| 45 | - { | |
| 46 | - field: 'state', | |
| 47 | - title: $t('foodLabeling.common.status'), | |
| 48 | - width: 90, | |
| 49 | - slots: { default: 'state' }, | |
| 32 | + slots: { default: 'creationTime' }, | |
| 50 | 33 | }, |
| 51 | - foodLabelingActionColumn({ minWidth: 160 }), | |
| 34 | + foodLabelingActionColumn({ minWidth: 120 }), | |
| 52 | 35 | ]; | ... | ... |
泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/views/food-labeling/platform/tenants/index.vue
| 1 | 1 | <script lang="ts" setup> |
| 2 | 2 | import type { VbenFormProps } from '@vben/common-ui'; |
| 3 | -import type { Recordable } from '@vben/types'; | |
| 4 | 3 | |
| 5 | 4 | import type { SaasTenantDto } from '../../shared/saas-types'; |
| 6 | 5 | |
| 7 | 6 | import type { VxeGridProps } from '#/adapter/vxe-table'; |
| 8 | 7 | |
| 9 | -import { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui'; | |
| 8 | +import { Page, useVbenModal } from '@vben/common-ui'; | |
| 10 | 9 | import { $t } from '@vben/locales'; |
| 11 | 10 | |
| 12 | -import { Alert, Avatar, Button, Dropdown, Menu, Modal, Space, Tag } from 'ant-design-vue'; | |
| 11 | +import { Alert, Avatar, Button, Modal, Space, Tooltip } from 'ant-design-vue'; | |
| 13 | 12 | |
| 14 | 13 | import { useVbenVxeGrid } from '#/adapter/vxe-table'; |
| 15 | 14 | |
| 16 | -import { matchKeyword, slicePage } from '../../shared/static-mode'; | |
| 17 | -import { MOCK_SAAS_TENANTS } from '../../shared/mock-platform-data'; | |
| 18 | 15 | import { |
| 19 | 16 | buildManagementTabFormOptions, |
| 20 | 17 | managementTabGridBase, |
| ... | ... | @@ -22,9 +19,8 @@ import { |
| 22 | 19 | managementTabTableClass, |
| 23 | 20 | managementTabVxeGridClass, |
| 24 | 21 | } from '../../management/shared/management-grid'; |
| 22 | +import { saasTenantRemove, saasTenantSelectList } from './api'; | |
| 25 | 23 | import { columns, querySchema } from './data'; |
| 26 | -import TenantAdminModal from './tenant-admin-modal.vue'; | |
| 27 | -import TenantMenuDrawer from './tenant-menu-drawer.vue'; | |
| 28 | 24 | import TenantModal from './tenant-modal.vue'; |
| 29 | 25 | |
| 30 | 26 | defineOptions({ name: 'FoodLabelingPlatformTenants' }); |
| ... | ... | @@ -44,23 +40,11 @@ const gridOptions: VxeGridProps = { |
| 44 | 40 | proxyConfig: { |
| 45 | 41 | ajax: { |
| 46 | 42 | query: async ({ page }, formValues = {}) => { |
| 47 | - let rows = [...MOCK_SAAS_TENANTS]; | |
| 48 | - const kw = String(formValues.Keyword ?? ''); | |
| 49 | - if (kw) { | |
| 50 | - rows = rows.filter((r) => | |
| 51 | - matchKeyword(r as Recordable, kw, [ | |
| 52 | - 'companyName', | |
| 53 | - 'companyCode', | |
| 54 | - 'adminUserName', | |
| 55 | - 'contactEmail', | |
| 56 | - 'contactName', | |
| 57 | - ]), | |
| 58 | - ); | |
| 59 | - } | |
| 60 | - if (formValues.State === true || formValues.State === false) { | |
| 61 | - rows = rows.filter((r) => r.state === formValues.State); | |
| 62 | - } | |
| 63 | - return slicePage(rows, page.currentPage, page.pageSize); | |
| 43 | + return saasTenantSelectList({ | |
| 44 | + Keyword: String(formValues.Keyword ?? '').trim() || undefined, | |
| 45 | + MaxResultCount: page.pageSize, | |
| 46 | + SkipCount: page.currentPage, | |
| 47 | + }); | |
| 64 | 48 | }, |
| 65 | 49 | }, |
| 66 | 50 | }, |
| ... | ... | @@ -82,14 +66,6 @@ const [TenantModalHost, tenantModalApi] = useVbenModal({ |
| 82 | 66 | connectedComponent: TenantModal, |
| 83 | 67 | }); |
| 84 | 68 | |
| 85 | -const [TenantMenuDrawerHost, tenantMenuDrawerApi] = useVbenDrawer({ | |
| 86 | - connectedComponent: TenantMenuDrawer, | |
| 87 | -}); | |
| 88 | - | |
| 89 | -const [TenantAdminModalHost, tenantAdminModalApi] = useVbenModal({ | |
| 90 | - connectedComponent: TenantAdminModal, | |
| 91 | -}); | |
| 92 | - | |
| 93 | 69 | function handleAdd() { |
| 94 | 70 | tenantModalApi.setData({}); |
| 95 | 71 | tenantModalApi.open(); |
| ... | ... | @@ -100,26 +76,9 @@ function handleEdit(row: SaasTenantDto) { |
| 100 | 76 | tenantModalApi.open(); |
| 101 | 77 | } |
| 102 | 78 | |
| 103 | -function handleMenus(row: SaasTenantDto) { | |
| 104 | - tenantMenuDrawerApi.setData({ id: row.id }); | |
| 105 | - tenantMenuDrawerApi.open(); | |
| 106 | -} | |
| 107 | - | |
| 108 | -function handleAdmin(row: SaasTenantDto) { | |
| 109 | - tenantAdminModalApi.setData({ id: row.id }); | |
| 110 | - tenantAdminModalApi.open(); | |
| 111 | -} | |
| 112 | - | |
| 113 | -function handleDelete(row: SaasTenantDto) { | |
| 114 | - const idx = MOCK_SAAS_TENANTS.findIndex((x) => x.id === row.id); | |
| 115 | - if (idx >= 0) { | |
| 116 | - MOCK_SAAS_TENANTS.splice(idx, 1); | |
| 117 | - } | |
| 118 | - tableApi.reload(); | |
| 119 | -} | |
| 120 | - | |
| 121 | -function menuCount(row: SaasTenantDto) { | |
| 122 | - return row.menuPermissionKeys?.length ?? 0; | |
| 79 | +async function handleDelete(row: SaasTenantDto) { | |
| 80 | + await saasTenantRemove(row.id); | |
| 81 | + await tableApi.reload(); | |
| 123 | 82 | } |
| 124 | 83 | |
| 125 | 84 | function confirmDelete(row: SaasTenantDto) { |
| ... | ... | @@ -129,27 +88,13 @@ function confirmDelete(row: SaasTenantDto) { |
| 129 | 88 | onOk: () => handleDelete(row), |
| 130 | 89 | }); |
| 131 | 90 | } |
| 132 | - | |
| 133 | -function onActionMenu(key: string, row: SaasTenantDto) { | |
| 134 | - if (key === 'menus') { | |
| 135 | - handleMenus(row); | |
| 136 | - return; | |
| 137 | - } | |
| 138 | - if (key === 'admin') { | |
| 139 | - handleAdmin(row); | |
| 140 | - return; | |
| 141 | - } | |
| 142 | - if (key === 'delete') { | |
| 143 | - confirmDelete(row); | |
| 144 | - } | |
| 145 | -} | |
| 146 | 91 | </script> |
| 147 | 92 | |
| 148 | 93 | <template> |
| 149 | 94 | <Page :auto-content-height="true" :content-class="managementTabPageContentClass"> |
| 150 | 95 | <Alert |
| 151 | 96 | class="mb-3 shrink-0" |
| 152 | - :message="$t('foodLabeling.platform.saasBanner')" | |
| 97 | + message="平台管理员:此处通过 tenant-select 查看租户,通过 provision 开通独立库租户;业务接口会依赖登录 Token 或 __tenant 请求头识别租户上下文。" | |
| 153 | 98 | show-icon |
| 154 | 99 | type="info" |
| 155 | 100 | /> |
| ... | ... | @@ -169,33 +114,20 @@ function onActionMenu(key: string, row: SaasTenantDto) { |
| 169 | 114 | </div> |
| 170 | 115 | </template> |
| 171 | 116 | |
| 172 | - <template #companyCode="{ row }"> | |
| 173 | - {{ displayText(row.companyCode) }} | |
| 117 | + <template #tenantId="{ row }"> | |
| 118 | + <span class="font-mono text-xs">{{ displayText(row.id) }}</span> | |
| 174 | 119 | </template> |
| 175 | 120 | |
| 176 | - <template #adminUserName="{ row }"> | |
| 177 | - <div class="truncate"> | |
| 178 | - <div>{{ displayText(row.adminFullName) }}</div> | |
| 179 | - <div class="text-xs text-gray-500">{{ displayText(row.adminUserName) }}</div> | |
| 180 | - </div> | |
| 121 | + <template #connectionString="{ row }"> | |
| 122 | + <Tooltip :title="row.remark || 'tenant-select 不返回连接串,编辑时读取 /tenant/{id}'"> | |
| 123 | + <span class="block max-w-[320px] truncate"> | |
| 124 | + {{ displayText(row.remark) }} | |
| 125 | + </span> | |
| 126 | + </Tooltip> | |
| 181 | 127 | </template> |
| 182 | 128 | |
| 183 | - <template #menuCount="{ row }"> | |
| 184 | - <Tag color="blue">{{ menuCount(row) }}</Tag> | |
| 185 | - </template> | |
| 186 | - | |
| 187 | - <template #contactEmail="{ row }"> | |
| 188 | - {{ displayText(row.contactEmail) }} | |
| 189 | - </template> | |
| 190 | - | |
| 191 | - <template #state="{ row }"> | |
| 192 | - <Tag :color="row.state !== false ? 'green' : 'default'"> | |
| 193 | - {{ | |
| 194 | - row.state !== false | |
| 195 | - ? $t('foodLabeling.common.enabled') | |
| 196 | - : $t('foodLabeling.common.disabled') | |
| 197 | - }} | |
| 198 | - </Tag> | |
| 129 | + <template #creationTime="{ row }"> | |
| 130 | + {{ displayText(row.creationTime) }} | |
| 199 | 131 | </template> |
| 200 | 132 | |
| 201 | 133 | <template #action="{ row }"> |
| ... | ... | @@ -204,33 +136,15 @@ function onActionMenu(key: string, row: SaasTenantDto) { |
| 204 | 136 | <ghost-button size="small" @click="handleEdit(row)"> |
| 205 | 137 | {{ $t('pages.common.edit') }} |
| 206 | 138 | </ghost-button> |
| 207 | - <Dropdown :trigger="['click']"> | |
| 208 | - <ghost-button size="small"> | |
| 209 | - {{ $t('pages.common.more') }} | |
| 210 | - </ghost-button> | |
| 211 | - <template #overlay> | |
| 212 | - <Menu @click="({ key }) => onActionMenu(String(key), row)"> | |
| 213 | - <Menu.Item key="menus"> | |
| 214 | - {{ $t('foodLabeling.platform.menuPermissions') }} | |
| 215 | - </Menu.Item> | |
| 216 | - <Menu.Item key="admin"> | |
| 217 | - {{ $t('foodLabeling.platform.tenantAdmin') }} | |
| 218 | - </Menu.Item> | |
| 219 | - <Menu.Divider /> | |
| 220 | - <Menu.Item key="delete" danger> | |
| 221 | - {{ $t('pages.common.delete') }} | |
| 222 | - </Menu.Item> | |
| 223 | - </Menu> | |
| 224 | - </template> | |
| 225 | - </Dropdown> | |
| 139 | + <ghost-button danger size="small" @click="confirmDelete(row)"> | |
| 140 | + {{ $t('pages.common.delete') }} | |
| 141 | + </ghost-button> | |
| 226 | 142 | </Space> |
| 227 | 143 | </div> |
| 228 | 144 | </template> |
| 229 | 145 | </BasicTable> |
| 230 | 146 | |
| 231 | 147 | <TenantModalHost @reload="tableApi.reload()" /> |
| 232 | - <TenantMenuDrawerHost @reload="tableApi.reload()" /> | |
| 233 | - <TenantAdminModalHost @reload="tableApi.reload()" /> | |
| 234 | 148 | </Page> |
| 235 | 149 | </template> |
| 236 | 150 | ... | ... |
泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/views/food-labeling/platform/tenants/tenant-admin-modal.vue
| ... | ... | @@ -5,14 +5,11 @@ import { computed, ref } from 'vue'; |
| 5 | 5 | |
| 6 | 6 | import { useVbenModal } from '@vben/common-ui'; |
| 7 | 7 | import { $t } from '@vben/locales'; |
| 8 | -import { cloneDeep } from '@vben/utils'; | |
| 9 | 8 | |
| 10 | 9 | import { message } from 'ant-design-vue'; |
| 11 | 10 | |
| 12 | 11 | import { useVbenForm } from '#/adapter/form'; |
| 13 | 12 | |
| 14 | -import { MOCK_SAAS_TENANTS } from '../../shared/mock-platform-data'; | |
| 15 | - | |
| 16 | 13 | const emit = defineEmits<{ reload: [] }>(); |
| 17 | 14 | |
| 18 | 15 | const tenantId = ref(''); |
| ... | ... | @@ -73,9 +70,9 @@ const [BasicModal, modalApi] = useVbenModal({ |
| 73 | 70 | if (!isOpen) { |
| 74 | 71 | return null; |
| 75 | 72 | } |
| 76 | - const data = modalApi.getData() as { id?: string }; | |
| 77 | - tenantId.value = data?.id ?? ''; | |
| 78 | - const record = MOCK_SAAS_TENANTS.find((x) => x.id === tenantId.value); | |
| 73 | + const data = modalApi.getData() as { row?: SaasTenantDto }; | |
| 74 | + const record = data.row ?? null; | |
| 75 | + tenantId.value = record?.id ?? ''; | |
| 79 | 76 | tenantName.value = record?.companyName ?? ''; |
| 80 | 77 | await formApi.setValues({ |
| 81 | 78 | adminUserName: record?.adminUserName ?? '', |
| ... | ... | @@ -91,18 +88,7 @@ async function handleConfirm() { |
| 91 | 88 | if (!valid || !tenantId.value) { |
| 92 | 89 | return; |
| 93 | 90 | } |
| 94 | - const values = cloneDeep(await formApi.getValues()) as Record<string, unknown>; | |
| 95 | - const idx = MOCK_SAAS_TENANTS.findIndex((x) => x.id === tenantId.value); | |
| 96 | - if (idx >= 0) { | |
| 97 | - const row = MOCK_SAAS_TENANTS[idx]!; | |
| 98 | - row.adminUserName = String(values.adminUserName ?? '').trim(); | |
| 99 | - row.adminFullName = String(values.adminFullName ?? '').trim(); | |
| 100 | - row.adminEmail = String(values.adminEmail ?? '').trim() || null; | |
| 101 | - if (!row.adminUserId) { | |
| 102 | - row.adminUserId = `admin-${tenantId.value.slice(0, 8)}`; | |
| 103 | - } | |
| 104 | - } | |
| 105 | - message.success($t('foodLabeling.common.staticDemoAction')); | |
| 91 | + message.warning('后端暂未提供公司管理员保存接口,当前仅展示真实租户信息。'); | |
| 106 | 92 | emit('reload'); |
| 107 | 93 | await handleCancel(); |
| 108 | 94 | } | ... | ... |
泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/views/food-labeling/platform/tenants/tenant-menu-drawer.vue
| ... | ... | @@ -8,7 +8,6 @@ import { $t } from '@vben/locales'; |
| 8 | 8 | |
| 9 | 9 | import { message } from 'ant-design-vue'; |
| 10 | 10 | |
| 11 | -import { MOCK_SAAS_TENANTS } from '../../shared/mock-platform-data'; | |
| 12 | 11 | import MenuPermissionTree from '../../shared/menu-permission-tree.vue'; |
| 13 | 12 | |
| 14 | 13 | const emit = defineEmits<{ reload: [] }>(); |
| ... | ... | @@ -24,8 +23,8 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({ |
| 24 | 23 | record.value = null; |
| 25 | 24 | return; |
| 26 | 25 | } |
| 27 | - const data = drawerApi.getData() as { id?: string }; | |
| 28 | - const found = MOCK_SAAS_TENANTS.find((x) => x.id === data?.id) ?? null; | |
| 26 | + const data = drawerApi.getData() as { row?: SaasTenantDto }; | |
| 27 | + const found = data.row ?? null; | |
| 29 | 28 | record.value = found; |
| 30 | 29 | menuKeys.value = found ? [...found.menuPermissionKeys] : []; |
| 31 | 30 | }, |
| ... | ... | @@ -35,10 +34,6 @@ async function handleSave() { |
| 35 | 34 | if (!record.value) { |
| 36 | 35 | return; |
| 37 | 36 | } |
| 38 | - const idx = MOCK_SAAS_TENANTS.findIndex((x) => x.id === record.value!.id); | |
| 39 | - if (idx >= 0) { | |
| 40 | - MOCK_SAAS_TENANTS[idx]!.menuPermissionKeys = [...menuKeys.value]; | |
| 41 | - } | |
| 42 | 37 | message.success($t('foodLabeling.platform.menuSaved')); |
| 43 | 38 | emit('reload'); |
| 44 | 39 | drawerApi.close(); | ... | ... |
泰额版/Food Labeling Management Code/Yi.Vben5.Vue3/apps/web-antd/src/views/food-labeling/platform/tenants/tenant-modal.vue
| 1 | 1 | <script lang="ts" setup> |
| 2 | -import type { SaasTenantDto } from '../../shared/saas-types'; | |
| 3 | - | |
| 4 | -import { computed, markRaw, ref } from 'vue'; | |
| 2 | +import { computed, ref } from 'vue'; | |
| 5 | 3 | |
| 6 | 4 | import { useVbenModal } from '@vben/common-ui'; |
| 7 | 5 | import { $t } from '@vben/locales'; |
| ... | ... | @@ -10,16 +8,16 @@ import { cloneDeep } from '@vben/utils'; |
| 10 | 8 | import { message } from 'ant-design-vue'; |
| 11 | 9 | |
| 12 | 10 | import { useVbenForm } from '#/adapter/form'; |
| 11 | +import { thProvisionTenant } from '#/api/th'; | |
| 13 | 12 | |
| 14 | -import { MOCK_SAAS_TENANTS } from '../../shared/mock-platform-data'; | |
| 15 | -import { stateModalSchema } from '../../shared/form-schema'; | |
| 16 | -import { ALL_SAAS_MENU_KEYS } from '../../shared/saas-menu-tree'; | |
| 17 | -import TenantLogoUploadField from './tenant-logo-upload-field.vue'; | |
| 13 | +import { saasTenantInfo, saasTenantUpdate } from './api'; | |
| 18 | 14 | |
| 19 | 15 | const emit = defineEmits<{ reload: [] }>(); |
| 20 | 16 | |
| 21 | 17 | const isUpdate = ref(false); |
| 22 | 18 | const recordId = ref(''); |
| 19 | +const entityVersion = ref<number | undefined>(); | |
| 20 | +const dbType = ref<number | undefined>(); | |
| 23 | 21 | |
| 24 | 22 | const title = computed(() => |
| 25 | 23 | isUpdate.value |
| ... | ... | @@ -37,96 +35,29 @@ const [BasicForm, formApi] = useVbenForm({ |
| 37 | 35 | { |
| 38 | 36 | component: 'Input', |
| 39 | 37 | fieldName: 'companyName', |
| 40 | - label: $t('foodLabeling.platform.companyName'), | |
| 41 | - rules: 'required', | |
| 42 | - }, | |
| 43 | - { | |
| 44 | - component: 'Input', | |
| 45 | - fieldName: 'companyCode', | |
| 46 | - label: $t('foodLabeling.platform.companyCode'), | |
| 38 | + label: '租户名称', | |
| 47 | 39 | rules: 'required', |
| 48 | - componentProps: { placeholder: 'SIAM_FRESH' }, | |
| 49 | - }, | |
| 50 | - { | |
| 51 | - component: markRaw(TenantLogoUploadField), | |
| 52 | - fieldName: 'logoUrl', | |
| 53 | - label: $t('foodLabeling.platform.logo'), | |
| 54 | - formItemClass: 'col-span-2 items-start', | |
| 55 | - componentProps: { class: 'w-auto' }, | |
| 56 | - }, | |
| 57 | - { | |
| 58 | - component: 'Input', | |
| 59 | - fieldName: 'contactName', | |
| 60 | - label: $t('foodLabeling.platform.contactName'), | |
| 61 | 40 | }, |
| 62 | 41 | { |
| 63 | 42 | component: 'Input', |
| 64 | - fieldName: 'contactEmail', | |
| 65 | - label: $t('foodLabeling.accountManagement.contactEmail'), | |
| 66 | - }, | |
| 67 | - { | |
| 68 | - component: 'Input', | |
| 69 | - fieldName: 'contactPhone', | |
| 70 | - label: $t('foodLabeling.accountManagement.phoneNumber'), | |
| 71 | - }, | |
| 72 | - { | |
| 73 | - component: 'Textarea', | |
| 74 | - fieldName: 'address', | |
| 75 | - label: $t('foodLabeling.platform.address'), | |
| 76 | - formItemClass: 'col-span-2', | |
| 77 | - }, | |
| 78 | - { | |
| 79 | - component: 'Textarea', | |
| 80 | - fieldName: 'remark', | |
| 81 | - label: $t('foodLabeling.accountManagement.remark'), | |
| 43 | + fieldName: 'tenantConnectionString', | |
| 44 | + label: '自定义连接串', | |
| 82 | 45 | formItemClass: 'col-span-2', |
| 83 | - }, | |
| 84 | - { | |
| 85 | - component: 'Input', | |
| 86 | - fieldName: 'adminUserName', | |
| 87 | - label: $t('foodLabeling.accountManagement.userName'), | |
| 88 | - rules: 'required', | |
| 89 | - dependencies: { | |
| 90 | - if: () => !isUpdate.value, | |
| 91 | - triggerFields: ['companyName'], | |
| 92 | - }, | |
| 93 | - }, | |
| 94 | - { | |
| 95 | - component: 'InputPassword', | |
| 96 | - fieldName: 'adminPassword', | |
| 97 | - label: $t('foodLabeling.accountManagement.password'), | |
| 98 | - rules: 'required', | |
| 99 | - dependencies: { | |
| 100 | - if: () => !isUpdate.value, | |
| 101 | - triggerFields: ['companyName'], | |
| 102 | - }, | |
| 103 | - }, | |
| 104 | - { | |
| 105 | - component: 'Input', | |
| 106 | - fieldName: 'adminFullName', | |
| 107 | - label: $t('foodLabeling.accountManagement.fullName'), | |
| 108 | - rules: 'required', | |
| 109 | - dependencies: { | |
| 110 | - if: () => !isUpdate.value, | |
| 111 | - triggerFields: ['companyName'], | |
| 46 | + componentProps: { | |
| 47 | + placeholder: '可选;为空时后端按租户名称自动生成业务库连接串', | |
| 112 | 48 | }, |
| 113 | 49 | }, |
| 114 | 50 | { |
| 115 | - component: 'Input', | |
| 116 | - fieldName: 'adminEmail', | |
| 117 | - label: $t('foodLabeling.accountManagement.contactEmail'), | |
| 51 | + component: 'Switch', | |
| 52 | + fieldName: 'initializeDatabase', | |
| 53 | + label: '初始化数据库', | |
| 54 | + defaultValue: true, | |
| 55 | + componentProps: { class: 'w-fit' }, | |
| 118 | 56 | dependencies: { |
| 119 | 57 | if: () => !isUpdate.value, |
| 120 | 58 | triggerFields: ['companyName'], |
| 121 | 59 | }, |
| 122 | 60 | }, |
| 123 | - ...stateModalSchema().map((item) => ({ | |
| 124 | - ...item, | |
| 125 | - componentProps: { | |
| 126 | - ...(item.componentProps as Record<string, unknown>), | |
| 127 | - class: 'w-fit', | |
| 128 | - }, | |
| 129 | - })), | |
| 130 | 61 | ], |
| 131 | 62 | showDefaultActions: false, |
| 132 | 63 | wrapperClass: 'grid-cols-2', |
| ... | ... | @@ -147,22 +78,18 @@ const [BasicModal, modalApi] = useVbenModal({ |
| 147 | 78 | recordId.value = data?.id ?? ''; |
| 148 | 79 | |
| 149 | 80 | if (isUpdate.value && recordId.value) { |
| 150 | - const record = | |
| 151 | - MOCK_SAAS_TENANTS.find((x) => x.id === recordId.value) ?? ({} as SaasTenantDto); | |
| 81 | + const record = await saasTenantInfo(recordId.value); | |
| 82 | + entityVersion.value = record.entityVersion; | |
| 83 | + dbType.value = record.dbType; | |
| 152 | 84 | await formApi.setValues({ |
| 153 | - companyName: record.companyName, | |
| 154 | - companyCode: record.companyCode, | |
| 155 | - logoUrl: record.logoUrl ?? '', | |
| 156 | - contactName: record.contactName ?? '', | |
| 157 | - contactEmail: record.contactEmail ?? '', | |
| 158 | - contactPhone: record.contactPhone ?? '', | |
| 159 | - address: record.address ?? '', | |
| 160 | - remark: record.remark ?? '', | |
| 161 | - state: record.state !== false, | |
| 85 | + companyName: record.name, | |
| 86 | + tenantConnectionString: record.tenantConnectionString ?? '', | |
| 162 | 87 | }); |
| 163 | 88 | } else { |
| 89 | + entityVersion.value = undefined; | |
| 90 | + dbType.value = undefined; | |
| 164 | 91 | await formApi.resetForm(); |
| 165 | - await formApi.setValues({ state: true }); | |
| 92 | + await formApi.setValues({ initializeDatabase: true }); | |
| 166 | 93 | } |
| 167 | 94 | modalApi.modalLoading(false); |
| 168 | 95 | }, |
| ... | ... | @@ -177,44 +104,26 @@ async function handleConfirm() { |
| 177 | 104 | } |
| 178 | 105 | const values = cloneDeep(await formApi.getValues()) as Record<string, unknown>; |
| 179 | 106 | if (isUpdate.value && recordId.value) { |
| 180 | - const idx = MOCK_SAAS_TENANTS.findIndex((x) => x.id === recordId.value); | |
| 181 | - if (idx >= 0) { | |
| 182 | - const prev = MOCK_SAAS_TENANTS[idx]!; | |
| 183 | - MOCK_SAAS_TENANTS[idx] = { | |
| 184 | - ...prev, | |
| 185 | - companyName: String(values.companyName ?? '').trim(), | |
| 186 | - companyCode: String(values.companyCode ?? '').trim(), | |
| 187 | - logoUrl: String(values.logoUrl ?? '').trim() || null, | |
| 188 | - contactName: String(values.contactName ?? '').trim() || null, | |
| 189 | - contactEmail: String(values.contactEmail ?? '').trim() || null, | |
| 190 | - contactPhone: String(values.contactPhone ?? '').trim() || null, | |
| 191 | - address: String(values.address ?? '').trim() || null, | |
| 192 | - remark: String(values.remark ?? '').trim() || null, | |
| 193 | - state: values.state !== false, | |
| 194 | - }; | |
| 195 | - } | |
| 107 | + await saasTenantUpdate({ | |
| 108 | + dbType: dbType.value, | |
| 109 | + entityVersion: entityVersion.value, | |
| 110 | + id: recordId.value, | |
| 111 | + name: String(values.companyName ?? '').trim(), | |
| 112 | + tenantConnectionString: String( | |
| 113 | + values.tenantConnectionString ?? '', | |
| 114 | + ).trim(), | |
| 115 | + }); | |
| 196 | 116 | } else { |
| 197 | - const id = crypto.randomUUID(); | |
| 198 | - MOCK_SAAS_TENANTS.push({ | |
| 199 | - id, | |
| 200 | - companyName: String(values.companyName ?? '').trim(), | |
| 201 | - companyCode: String(values.companyCode ?? '').trim(), | |
| 202 | - logoUrl: String(values.logoUrl ?? '').trim() || null, | |
| 203 | - contactName: String(values.contactName ?? '').trim() || null, | |
| 204 | - contactEmail: String(values.contactEmail ?? '').trim() || null, | |
| 205 | - contactPhone: String(values.contactPhone ?? '').trim() || null, | |
| 206 | - address: String(values.address ?? '').trim() || null, | |
| 207 | - remark: String(values.remark ?? '').trim() || null, | |
| 208 | - state: values.state !== false, | |
| 209 | - menuPermissionKeys: [...ALL_SAAS_MENU_KEYS], | |
| 210 | - adminUserId: `admin-${id.slice(0, 8)}`, | |
| 211 | - adminUserName: String(values.adminUserName ?? '').trim(), | |
| 212 | - adminFullName: String(values.adminFullName ?? '').trim(), | |
| 213 | - adminEmail: String(values.adminEmail ?? '').trim() || null, | |
| 214 | - creationTime: new Date().toISOString().slice(0, 16).replace('T', ' '), | |
| 117 | + await thProvisionTenant({ | |
| 118 | + dbType: 0, | |
| 119 | + initializeDatabase: values.initializeDatabase !== false, | |
| 120 | + name: String(values.companyName ?? '').trim(), | |
| 121 | + tenantConnectionString: String( | |
| 122 | + values.tenantConnectionString ?? '', | |
| 123 | + ).trim(), | |
| 215 | 124 | }); |
| 216 | 125 | } |
| 217 | - message.success($t('foodLabeling.common.staticDemoAction')); | |
| 126 | + message.success($t('http.operationSuccess')); | |
| 218 | 127 | emit('reload'); |
| 219 | 128 | await handleCancel(); |
| 220 | 129 | } finally { | ... | ... |
美国版/Food Labeling Management App UniApp/.env.development
美国版/Food Labeling Management Platform/.env.local
美国版/Food Labeling Management Platform/src/components/shared/category-scope-fields.tsx
| ... | ... | @@ -9,6 +9,7 @@ import { |
| 9 | 9 | regionOptionsForPartner, |
| 10 | 10 | filterScopeIdsForPartners, |
| 11 | 11 | partnerIdsScopeKey, |
| 12 | + regionNamesToGroupIds, | |
| 12 | 13 | regionOptionsForPartners, |
| 13 | 14 | regionSelectionToGroupNames, |
| 14 | 15 | } from "../../lib/categoryScopeForm"; |
| ... | ... | @@ -120,6 +121,13 @@ export function CategoryScopeFields({ |
| 120 | 121 | return regionOptionsForPartner(groups, effectivePartnerId); |
| 121 | 122 | }, [templateScopeMode, groups, effectivePartnerIds, effectivePartnerId]); |
| 122 | 123 | |
| 124 | + const selectedRegionPickerValues = useMemo(() => { | |
| 125 | + if (templateScopeMode) return selectedRegionIds; | |
| 126 | + const ids = regionNamesToGroupIds(selectedRegionNames, groups, effectivePartnerId); | |
| 127 | + if (ids.length > 0) return ids; | |
| 128 | + return selectedRegionNames.map((x) => x.trim()).filter(Boolean); | |
| 129 | + }, [templateScopeMode, selectedRegionIds, selectedRegionNames, groups, effectivePartnerId]); | |
| 130 | + | |
| 123 | 131 | const scopedLocations = useMemo(() => { |
| 124 | 132 | if (templateScopeMode) { |
| 125 | 133 | return locationsScopedForTemplateScope( |
| ... | ... | @@ -332,7 +340,7 @@ export function CategoryScopeFields({ |
| 332 | 340 | ) : null} |
| 333 | 341 | <TemplateFieldGroup label="Region:" className="min-w-[10rem] flex-[1_1_10rem]"> |
| 334 | 342 | <SearchableMultiSelect |
| 335 | - values={templateScopeMode ? selectedRegionIds : selectedRegionNames} | |
| 343 | + values={selectedRegionPickerValues} | |
| 336 | 344 | onValuesChange={templateScopeMode ? onRegionIdsMultiChange : onRegionMultiChange} |
| 337 | 345 | options={regionOptions} |
| 338 | 346 | placeholder={regionPh} |
| ... | ... | @@ -401,7 +409,7 @@ export function CategoryScopeFields({ |
| 401 | 409 | <div className={scopeCellClass}> |
| 402 | 410 | <Label className={labelClass}>Region *</Label> |
| 403 | 411 | <SearchableMultiSelect |
| 404 | - values={selectedRegionNames} | |
| 412 | + values={selectedRegionPickerValues} | |
| 405 | 413 | onValuesChange={onRegionMultiChange} |
| 406 | 414 | options={regionOptions} |
| 407 | 415 | placeholder={regionPh} |
| ... | ... | @@ -466,7 +474,7 @@ export function CategoryScopeFields({ |
| 466 | 474 | <div className="space-y-2"> |
| 467 | 475 | <Label>Region *</Label> |
| 468 | 476 | <SearchableMultiSelect |
| 469 | - values={templateScopeMode ? selectedRegionIds : selectedRegionNames} | |
| 477 | + values={selectedRegionPickerValues} | |
| 470 | 478 | onValuesChange={templateScopeMode ? onRegionIdsMultiChange : onRegionMultiChange} |
| 471 | 479 | options={regionOptions} |
| 472 | 480 | placeholder={regionPlaceholder} | ... | ... |