Commit 4950e3a0af6e6960f33c62929b94ffd6777b904f

Authored by 杨鑫
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
1 1 export * from './th-app-auth';
2 2 export * from './th-multi-tenancy';
  3 +export * from './th-tenant-provisioning';
  4 +export * from './th-web-auth';
... ...
泰额版/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 &#39;@vben/stores&#39;;
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(&#39;auth&#39;, () =&gt; {
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(&#39;auth&#39;, () =&gt; {
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(&#39;auth&#39;, () =&gt; {
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(&#39;auth&#39;, () =&gt; {
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[] =&gt; {
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[] =&gt; {
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 &#39;vue&#39;;
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 &#39;@vben/locales&#39;;
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 &#39;@vben/utils&#39;;
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
1 1 # H5 / App 开发:直连本地后端(按本机 IP 与端口修改)
2   -VITE_US_API_BASE=http://192.168.31.89:19001
  2 +VITE_US_API_BASE=http://192.168.31.87:19001
... ...
美国版/Food Labeling Management Platform/.env.local
1   -# VITE_API_BASE_URL=http://192.168.31.89:19001
2   - VITE_API_BASE_URL=http://flus-test.3ffoodsafety.com
  1 + VITE_API_BASE_URL=http://192.168.31.87:19001
  2 +# VITE_API_BASE_URL=http://flus-test.3ffoodsafety.com
... ...
美国版/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}
... ...