Эх сурвалжийг харах

feat: 新增进入租管、切换租户功能

喵你个旺呀 1 жил өмнө
parent
commit
27cc0aadf6

+ 2 - 0
Admin.NET/Admin.NET.Core/SeedData/SysMenuSeedData.cs

@@ -100,6 +100,8 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
             new SysMenu{ Id=1310000000317, Pid=1310000000311, Title="重置密码", Permission="sysTenant:resetPwd", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
             new SysMenu{ Id=1310000000318, Pid=1310000000311, Title="生成库", Permission="sysTenant:createDb", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
             new SysMenu{ Id=1310000000319, Pid=1310000000311, Title="设置状态", Permission="sysTenant:setStatus", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
+            new SysMenu{ Id=1310000001320, Pid=1310000000311, Title="切换租户", Permission="sysTenant:changeTenant", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
+            new SysMenu{ Id=1310000001321, Pid=1310000000311, Title="进入租管端", Permission="sysTenant:goTenant", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
 
             new SysMenu{ Id=1310000000321, Pid=1310000000301, Title="菜单管理", Path="/platform/menu", Name="sysMenu", Component="/system/menu/index", Icon="ele-Menu", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=110 },
             new SysMenu{ Id=1310000000322, Pid=1310000000321, Title="查询", Permission="sysMenu:list", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },

+ 52 - 0
Admin.NET/Admin.NET.Core/Service/Tenant/SysTenantService.cs

@@ -454,6 +454,58 @@ public class SysTenantService : IDynamicApiController, ITransient
         await _sysUserRep.UpdateAsync(u => new SysUser { Password = encryptPassword }, u => u.Id == input.UserId);
         return password;
     }
+    
+    /// <summary>
+    /// 切换租户 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [UnitOfWork]
+    [DisplayName("切换租户")]
+    public async Task<LoginOutput> ChangeTenant(BaseIdInput input)
+    {
+        var userManager = App.GetService<UserManager>();
+
+        _ = await _sysTenantRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
+        var user = await _sysUserRep.GetFirstAsync(u => u.Id == userManager.UserId) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
+        user.TenantId = input.Id;
+
+        return await GetAccessTokenInNotSingleLogin(user);
+    }
+    
+    /// <summary>
+    /// 进入租管端 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [DisplayName("进入租管端")]
+    public async Task<LoginOutput> GoTenant(BaseIdInput input)
+    {
+        var tenant = await _sysTenantRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
+        var user = await _sysUserRep.GetFirstAsync(u => u.Id == tenant.UserId) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
+        return await GetAccessTokenInNotSingleLogin(user);
+    }
+    
+    /// <summary>
+    /// 在非单用户登录模式下获取登录令牌
+    /// </summary>
+    /// <param name="user"></param>
+    /// <returns></returns>
+    [NonAction]
+    public async Task<LoginOutput> GetAccessTokenInNotSingleLogin(SysUser user)
+    {
+        // 使用非单用户模式登录
+        var singleLogin = _sysCacheService.Get<bool>($"{CacheConst.KeyConfig}{ConfigConst.SysSingleLogin}");
+        try
+        {
+            return await App.GetService<SysAuthService>().CreateToken(user);
+        }
+        finally
+        {
+            // 恢复单用户登录参数
+            if (singleLogin) _sysCacheService.Set($"{CacheConst.KeyConfig}{ConfigConst.SysSingleLogin}", true);
+        }
+    }
 
     /// <summary>
     /// 缓存所有租户

+ 168 - 0
Web/src/api-services/apis/sys-tenant-api.ts

@@ -20,9 +20,11 @@ import { AddTenantInput } from '../models';
 import { AdminResultInt32 } from '../models';
 import { AdminResultListInt64 } from '../models';
 import { AdminResultListSysUser } from '../models';
+import { AdminResultLoginOutput } from '../models';
 import { AdminResultObject } from '../models';
 import { AdminResultSqlSugarPagedListTenantOutput } from '../models';
 import { AdminResultString } from '../models';
+import { BaseIdInput } from '../models';
 import { DeleteTenantInput } from '../models';
 import { PageTenantInput } from '../models';
 import { TenantIdInput } from '../models';
@@ -84,6 +86,54 @@ export const SysTenantApiAxiosParamCreator = function (configuration?: Configura
                 options: localVarRequestOptions,
             };
         },
+        /**
+         * 
+         * @summary 切换租户 🔖
+         * @param {BaseIdInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysTenantChangeTenantPost: async (body?: BaseIdInput, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/sysTenant/changeTenant`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, 'https://example.com');
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+            const localVarRequestOptions :AxiosRequestConfig = { method: 'POST', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication Bearer required
+            // http bearer authentication required
+            if (configuration && configuration.accessToken) {
+                const accessToken = typeof configuration.accessToken === 'function'
+                    ? await configuration.accessToken()
+                    : await configuration.accessToken;
+                localVarHeaderParameter["Authorization"] = "Bearer " + accessToken;
+            }
+
+            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';
+
+            const query = new URLSearchParams(localVarUrlObj.search);
+            for (const key in localVarQueryParameter) {
+                query.set(key, localVarQueryParameter[key]);
+            }
+            for (const key in options.params) {
+                query.set(key, options.params[key]);
+            }
+            localVarUrlObj.search = (new URLSearchParams(query)).toString();
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+            const needsSerialization = (typeof body !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json';
+            localVarRequestOptions.data =  needsSerialization ? JSON.stringify(body !== undefined ? body : {}) : (body || "");
+
+            return {
+                url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash,
+                options: localVarRequestOptions,
+            };
+        },
         /**
          * 
          * @summary 创建租户数据库 🔖
@@ -180,6 +230,54 @@ export const SysTenantApiAxiosParamCreator = function (configuration?: Configura
                 options: localVarRequestOptions,
             };
         },
+        /**
+         * 
+         * @summary 进入租管端 🔖
+         * @param {BaseIdInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysTenantGoTenantPost: async (body?: BaseIdInput, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/sysTenant/goTenant`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, 'https://example.com');
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+            const localVarRequestOptions :AxiosRequestConfig = { method: 'POST', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication Bearer required
+            // http bearer authentication required
+            if (configuration && configuration.accessToken) {
+                const accessToken = typeof configuration.accessToken === 'function'
+                    ? await configuration.accessToken()
+                    : await configuration.accessToken;
+                localVarHeaderParameter["Authorization"] = "Bearer " + accessToken;
+            }
+
+            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';
+
+            const query = new URLSearchParams(localVarUrlObj.search);
+            for (const key in localVarQueryParameter) {
+                query.set(key, localVarQueryParameter[key]);
+            }
+            for (const key in options.params) {
+                query.set(key, options.params[key]);
+            }
+            localVarUrlObj.search = (new URLSearchParams(query)).toString();
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+            const needsSerialization = (typeof body !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json';
+            localVarRequestOptions.data =  needsSerialization ? JSON.stringify(body !== undefined ? body : {}) : (body || "");
+
+            return {
+                url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash,
+                options: localVarRequestOptions,
+            };
+        },
         /**
          * 
          * @summary 授权租户菜单 🔖
@@ -586,6 +684,20 @@ export const SysTenantApiFp = function(configuration?: Configuration) {
                 return axios.request(axiosRequestArgs);
             };
         },
+        /**
+         * 
+         * @summary 切换租户 🔖
+         * @param {BaseIdInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysTenantChangeTenantPost(body?: BaseIdInput, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminResultLoginOutput>>> {
+            const localVarAxiosArgs = await SysTenantApiAxiosParamCreator(configuration).apiSysTenantChangeTenantPost(body, options);
+            return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
+                const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
+                return axios.request(axiosRequestArgs);
+            };
+        },
         /**
          * 
          * @summary 创建租户数据库 🔖
@@ -614,6 +726,20 @@ export const SysTenantApiFp = function(configuration?: Configuration) {
                 return axios.request(axiosRequestArgs);
             };
         },
+        /**
+         * 
+         * @summary 进入租管端 🔖
+         * @param {BaseIdInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysTenantGoTenantPost(body?: BaseIdInput, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminResultLoginOutput>>> {
+            const localVarAxiosArgs = await SysTenantApiAxiosParamCreator(configuration).apiSysTenantGoTenantPost(body, options);
+            return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
+                const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
+                return axios.request(axiosRequestArgs);
+            };
+        },
         /**
          * 
          * @summary 授权租户菜单 🔖
@@ -744,6 +870,16 @@ export const SysTenantApiFactory = function (configuration?: Configuration, base
         async apiSysTenantAddPost(body?: AddTenantInput, options?: AxiosRequestConfig): Promise<AxiosResponse<void>> {
             return SysTenantApiFp(configuration).apiSysTenantAddPost(body, options).then((request) => request(axios, basePath));
         },
+        /**
+         * 
+         * @summary 切换租户 🔖
+         * @param {BaseIdInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysTenantChangeTenantPost(body?: BaseIdInput, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminResultLoginOutput>> {
+            return SysTenantApiFp(configuration).apiSysTenantChangeTenantPost(body, options).then((request) => request(axios, basePath));
+        },
         /**
          * 
          * @summary 创建租户数据库 🔖
@@ -764,6 +900,16 @@ export const SysTenantApiFactory = function (configuration?: Configuration, base
         async apiSysTenantDeletePost(body?: DeleteTenantInput, options?: AxiosRequestConfig): Promise<AxiosResponse<void>> {
             return SysTenantApiFp(configuration).apiSysTenantDeletePost(body, options).then((request) => request(axios, basePath));
         },
+        /**
+         * 
+         * @summary 进入租管端 🔖
+         * @param {BaseIdInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysTenantGoTenantPost(body?: BaseIdInput, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminResultLoginOutput>> {
+            return SysTenantApiFp(configuration).apiSysTenantGoTenantPost(body, options).then((request) => request(axios, basePath));
+        },
         /**
          * 
          * @summary 授权租户菜单 🔖
@@ -864,6 +1010,17 @@ export class SysTenantApi extends BaseAPI {
     public async apiSysTenantAddPost(body?: AddTenantInput, options?: AxiosRequestConfig) : Promise<AxiosResponse<void>> {
         return SysTenantApiFp(this.configuration).apiSysTenantAddPost(body, options).then((request) => request(this.axios, this.basePath));
     }
+    /**
+     * 
+     * @summary 切换租户 🔖
+     * @param {BaseIdInput} [body] 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysTenantApi
+     */
+    public async apiSysTenantChangeTenantPost(body?: BaseIdInput, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminResultLoginOutput>> {
+        return SysTenantApiFp(this.configuration).apiSysTenantChangeTenantPost(body, options).then((request) => request(this.axios, this.basePath));
+    }
     /**
      * 
      * @summary 创建租户数据库 🔖
@@ -886,6 +1043,17 @@ export class SysTenantApi extends BaseAPI {
     public async apiSysTenantDeletePost(body?: DeleteTenantInput, options?: AxiosRequestConfig) : Promise<AxiosResponse<void>> {
         return SysTenantApiFp(this.configuration).apiSysTenantDeletePost(body, options).then((request) => request(this.axios, this.basePath));
     }
+    /**
+     * 
+     * @summary 进入租管端 🔖
+     * @param {BaseIdInput} [body] 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysTenantApi
+     */
+    public async apiSysTenantGoTenantPost(body?: BaseIdInput, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminResultLoginOutput>> {
+        return SysTenantApiFp(this.configuration).apiSysTenantGoTenantPost(body, options).then((request) => request(this.axios, this.basePath));
+    }
     /**
      * 
      * @summary 授权租户菜单 🔖

+ 26 - 0
Web/src/api-services/models/base-id-input.ts

@@ -0,0 +1,26 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+ * Admin.NET 通用权限开发平台
+ * 让 .NET 开发更简单、更通用、更流行。整合最新技术,模块插件式开发,前后端分离,开箱即用。<br/><u><b><font color='FF0000'> 👮不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!</font></b></u>
+ *
+ * OpenAPI spec version: 1.0.0
+ * 
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ * Do not edit the class manually.
+ */
+/**
+ * 主键Id输入参数
+ * @export
+ * @interface BaseIdInput
+ */
+export interface BaseIdInput {
+    /**
+     * 主键Id
+     * @type {number}
+     * @memberof BaseIdInput
+     */
+    id: number;
+}

+ 1 - 0
Web/src/api-services/models/index.ts

@@ -113,6 +113,7 @@ export * from './admin-result-wx-open-id-output';
 export * from './admin-result-wx-phone-output';
 export * from './api-output';
 export * from './assembly';
+export * from './base-id-input';
 export * from './base-proc-input';
 export * from './batch-config-input';
 export * from './calendar';

+ 1 - 0
Web/src/i18n/lang/en.ts

@@ -151,6 +151,7 @@ export default {
 		threeIsUniqueOpened: 'Menu accordion',
 		threeIsFixedHeader: 'Fixed header',
 		threeIsClassicSplitMenu: 'Classic layout split menu',
+		changeTenant: 'Change tenant',
 		threeIsLockScreen: 'Open the lock screen',
 		threeLockScreenTime: 'screen locking(s/s)',
 		fourTitle: 'Interface display',

+ 1 - 0
Web/src/i18n/lang/zh-cn.ts

@@ -151,6 +151,7 @@ export default {
 		threeIsUniqueOpened: '菜单手风琴',
 		threeIsFixedHeader: '固定 Header',
 		threeIsClassicSplitMenu: '经典布局分割菜单',
+		changeTenant: '切换租户',
 		threeIsLockScreen: '开启锁屏',
 		threeLockScreenTime: '自动锁屏(s/秒)',
 		fourTitle: '界面显示',

+ 1 - 0
Web/src/i18n/lang/zh-tw.ts

@@ -151,6 +151,7 @@ export default {
 		threeIsUniqueOpened: '選單手風琴',
 		threeIsFixedHeader: '固定 Header',
 		threeIsClassicSplitMenu: '經典佈局分割選單',
+		changeTenant: '切換租戶',
 		threeIsLockScreen: '開啟鎖屏',
 		threeLockScreenTime: '自動鎖屏(s/秒)',
 		fourTitle: '介面顯示',

+ 74 - 0
Web/src/layout/navBars/topBar/changeTenant.vue

@@ -0,0 +1,74 @@
+<template>
+	<div class="sys-app-container">
+		<el-dialog v-model="state.isShowDialog" width="300" draggable :close-on-click-modal="false">
+			<template #header>
+				<div style="color: #fff">
+					<el-icon size="16" style="margin-right: 3px; display: inline; vertical-align: middle"> <ele-Switch /> </el-icon>
+					<span>切换租户</span>
+				</div>
+			</template>
+			<el-form :model="state.ruleForm" ref="ruleFormRef" label-width="auto">
+				<el-row :gutter="35">
+					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
+						<el-form-item label="租户" prop="id" :rules="[{ required: true, message: '租户不能为空', trigger: 'blur' }]">
+							<el-select v-model="state.ruleForm.id" value-key="id" placeholder="租户" class="w100">
+								<el-option v-for="(item, index) in state.tenantList" :key="index" :label="`${item.label ?? '默认'} (${item.host})`" :value="item.value" />
+							</el-select>
+						</el-form-item>
+					</el-col>
+				</el-row>
+			</el-form>
+			<template #footer>
+				<span class="dialog-footer">
+					<el-button @click="() => state.isShowDialog = false">取 消</el-button>
+					<el-button type="primary" @click="submit">确 定</el-button>
+				</span>
+			</template>
+		</el-dialog>
+	</div>
+</template>
+
+<script lang="ts" setup name="sysEditApp">
+import { reactive, ref } from 'vue';
+import { Local } from "/@/utils/storage";
+import { getAPI } from '/@/utils/axios-utils';
+import { SysTenantApi } from "/@/api-services";
+import { accessTokenKey, refreshAccessTokenKey } from "/@/utils/request";
+
+const ruleFormRef = ref();
+const state = reactive({
+	loading: false,
+	isShowDialog: false,
+	ruleForm: {} as any,
+	appList: [] as Array<any>,
+	tenantList: [] as Array<any>
+});
+
+// 打开弹窗
+const openDialog = async () => {
+	state.tenantList = await getAPI(SysTenantApi).apiSysTenantListGet().then(res => res.data.result ?? []);
+	state.ruleForm.id = state.tenantList[0].value;
+	ruleFormRef.value?.resetFields();
+	state.isShowDialog = true;
+	state.loading = false;
+};
+
+// 提交
+const submit = () => {
+	ruleFormRef.value.validate(async (valid: boolean) => {
+		if (!valid) return;
+		state.loading = true;
+		const newToken = await getAPI(SysTenantApi).apiSysTenantChangeTenantPost(state.ruleForm).then(res => res.data.result);
+		if (newToken) {
+			Local.set(accessTokenKey, newToken.accessToken);
+			Local.set(refreshAccessTokenKey, newToken.refreshToken);
+			location.href = "/";
+		}
+		state.loading = false;
+		state.isShowDialog = false;
+	});
+};
+
+// 导出对象
+defineExpose({ openDialog });
+</script>

+ 7 - 1
Web/src/layout/navBars/topBar/user.vue

@@ -75,6 +75,7 @@
 					<!-- <el-dropdown-item command="/dashboard/home">{{ $t('message.user.dropdown1') }}</el-dropdown-item> -->
 					<el-dropdown-item :icon="Avatar" command="/system/userCenter">{{ $t('message.user.dropdown2') }}</el-dropdown-item>
 					<el-dropdown-item :icon="Loading" command="clearCache">{{ $t('message.user.dropdown3') }}</el-dropdown-item>
+					<el-dropdown-item :icon="Switch" divided command="changeTenant" v-if="auth('sysTenant:changeTenant')">{{ $t('message.layout.changeTenant') }}</el-dropdown-item>
 					<el-dropdown-item :icon="Lock" divided command="lockScreen">{{ $t('message.layout.threeIsLockScreen') }}</el-dropdown-item>
 					<el-dropdown-item :icon="CircleCloseFilled" divided command="logOut">{{ $t('message.user.dropdown5') }}</el-dropdown-item>
 				</el-dropdown-menu>
@@ -82,6 +83,7 @@
 		</el-dropdown>
 		<Search ref="searchRef" />
 		<OnlineUser ref="onlineUserRef" />
+		<ChangeTenant ref="changeTenantRef" />
 	</div>
 </template>
 
@@ -100,14 +102,15 @@ import { Local, Session } from '/@/utils/storage';
 import Push from 'push.js';
 import { signalR } from '/@/views/system/onlineUser/signalR';
 import { Avatar, CircleCloseFilled, Loading, Lock, Switch } from '@element-plus/icons-vue';
-
 import { clearAccessTokens, getAPI } from '/@/utils/axios-utils';
 import { SysAuthApi, SysNoticeApi } from '/@/api-services/api';
+import { auth } from "/@/utils/authFunction";
 
 // 引入组件
 const UserNews = defineAsyncComponent(() => import('/@/layout/navBars/topBar/userNews.vue'));
 const Search = defineAsyncComponent(() => import('/@/layout/navBars/topBar/search.vue'));
 const OnlineUser = defineAsyncComponent(() => import('/@/views/system/onlineUser/index.vue'));
+const ChangeTenant = defineAsyncComponent(() => import('./changeTenant.vue'));
 
 // 定义变量内容
 const { locale, t } = useI18n();
@@ -118,6 +121,7 @@ const { userInfos } = storeToRefs(stores);
 const { themeConfig } = storeToRefs(storesThemeConfig);
 const searchRef = ref();
 const onlineUserRef = ref();
+const changeTenantRef = ref();
 const state = reactive({
 	isScreenfull: false,
 	disabledI18n: 'zh-cn',
@@ -191,6 +195,8 @@ const onHandleCommandClick = (path: string) => {
 				clearAccessTokens();
 			})
 			.catch(() => {});
+	} else if (path === 'changeTenant') {
+		changeTenantRef.value?.openDialog();
 	} else {
 		router.push(path);
 	}

+ 42 - 0
Web/src/views/system/tenant/index.vue

@@ -104,6 +104,10 @@
 								style="padding-left: 12px" />
 							<template #dropdown>
 								<el-dropdown-menu>
+									<el-dropdown-item icon="ele-OfficeBuilding" @click="goTenant(scope.row)"
+									                  :v-auth="'sysTenant:goTenant'"> 进入租管端 </el-dropdown-item>
+									<el-dropdown-item icon="ele-OfficeBuilding" @click="changeTenant(scope.row)"
+									                  :v-auth="'sysTenant:changeTenant'"> 切换租户 </el-dropdown-item>
 									<el-dropdown-item icon="ele-OfficeBuilding" @click="openGrantMenu(scope.row)"
 										:v-auth="'sysTenant:grantMenu'"> 授权菜单 </el-dropdown-item>
 									<el-dropdown-item icon="ele-RefreshLeft" @click="resetTenantPwd(scope.row)"
@@ -136,6 +140,8 @@ import ModifyRecord from '/@/components/table/modifyRecord.vue';
 import { getAPI } from '/@/utils/axios-utils';
 import { SysTenantApi } from '/@/api-services/api';
 import { TenantOutput } from '/@/api-services/models';
+import {Local} from "/@/utils/storage";
+import {accessTokenKey, refreshAccessTokenKey} from "/@/utils/request";
 
 const editTenantRef = ref<InstanceType<typeof EditTenant>>();
 const grantMenuRef = ref<InstanceType<typeof GrantMenu>>();
@@ -168,6 +174,42 @@ const handleQuery = async () => {
 	state.loading = false;
 };
 
+// 进入租管端
+const goTenant = (row: any) => {
+	ElMessageBox.confirm(`确定要进入【${row.name}】租管端?`, '提示', {
+		confirmButtonText: '确定',
+		cancelButtonText: '取消',
+		type: 'warning',
+	}).then(async () => {
+		await getAPI(SysTenantApi).apiSysTenantGoTenantPost({ id: row.id }).then(res => {
+			const newToken = res.data.result;
+			if (newToken) {
+				Local.set(accessTokenKey, newToken.accessToken);
+				Local.set(refreshAccessTokenKey, newToken.refreshToken);
+				location.href = "/";
+			}
+		});
+	});
+}
+
+// 切换租户
+const changeTenant = (row: any) => {
+	ElMessageBox.confirm(`确定要将当前用户切换到【${row.name}】?`, '提示', {
+		confirmButtonText: '确定',
+		cancelButtonText: '取消',
+		type: 'warning',
+	}).then(async () => {
+		await getAPI(SysTenantApi).apiSysTenantChangeTenantPost({ id: row.id }).then(res => {
+			const newToken = res.data.result;
+			if (newToken) {
+				Local.set(accessTokenKey, newToken.accessToken);
+				Local.set(refreshAccessTokenKey, newToken.refreshToken);
+				location.href = "/";
+			}
+		});
+	});
+}
+
 // 重置操作
 const resetQuery = () => {
 	state.queryParams.name = undefined;