diff --git a/.env.development b/.env.development index 8dbca7fd..898b5114 100644 --- a/.env.development +++ b/.env.development @@ -6,8 +6,8 @@ VITE_APP_TITLE=vue3-element-admin VITE_APP_BASE_API=/dev-api # 接口地址 -VITE_APP_API_URL=https://api.youlai.tech # 线上 -# VITE_APP_API_URL=http://localhost:8989 # 本地 +# VITE_APP_API_URL=https://api.youlai.tech # 线上 +VITE_APP_API_URL=http://localhost:8000 # 本地 # WebSocket 端点(不配置则关闭),线上 ws://api.youlai.tech/ws ,本地 ws://localhost:8989/ws VITE_APP_WS_ENDPOINT= diff --git a/src/api/auth-api.ts b/src/api/auth-api.ts index 772e9ef3..d2e9e3f6 100644 --- a/src/api/auth-api.ts +++ b/src/api/auth-api.ts @@ -4,18 +4,16 @@ const AUTH_BASE_URL = "/api/v1/auth"; const AuthAPI = { /** 登录接口*/ - login(data: LoginFormData) { - const formData = new FormData(); - formData.append("username", data.username); - formData.append("password", data.password); - formData.append("captchaKey", data.captchaKey); - formData.append("captchaCode", data.captchaCode); + login(data: LoginRequest) { return request({ url: `${AUTH_BASE_URL}/login`, method: "post", - data: formData, - headers: { - "Content-Type": "multipart/form-data", + data: { + username: data.username, + password: data.password, + captchaId: data.captchaId, + captchaCode: data.captchaCode, + tenantId: data.tenantId, }, }); }, @@ -51,18 +49,20 @@ const AuthAPI = { export default AuthAPI; -/** 登录表单数据 */ -export interface LoginFormData { +/** 登录请求参数 */ +export interface LoginRequest { /** 用户名 */ username: string; /** 密码 */ password: string; - /** 验证码缓存key */ - captchaKey: string; + /** 验证码缓存ID */ + captchaId?: string; /** 验证码 */ - captchaCode: string; - /** 记住我 */ - rememberMe: boolean; + captchaCode?: string; + /** 记住我(前端使用,不发送到后端) */ + rememberMe?: boolean; + /** 租户ID(可选,多租户模式下用于指定租户) */ + tenantId?: number; } /** 登录响应 */ @@ -79,8 +79,8 @@ export interface LoginResult { /** 验证码信息 */ export interface CaptchaInfo { - /** 验证码缓存key */ - captchaKey: string; + /** 验证码缓存ID */ + captchaId: string; /** 验证码图片Base64字符串 */ captchaBase64: string; } diff --git a/src/api/system/tenant-api.ts b/src/api/system/tenant-api.ts index 92e902c9..1043a993 100644 --- a/src/api/system/tenant-api.ts +++ b/src/api/system/tenant-api.ts @@ -1,6 +1,6 @@ import request from "@/utils/request"; -const TENANT_BASE_URL = "/api/v1/tenant"; +const TENANT_BASE_URL = "/api/v1/tenants"; /** * 租户信息 @@ -37,7 +37,7 @@ const TenantAPI = { */ getTenantList() { return request({ - url: `${TENANT_BASE_URL}/list`, + url: `${TENANT_BASE_URL}`, method: "get", }); }, @@ -59,7 +59,7 @@ const TenantAPI = { */ switchTenant(tenantId: number) { return request({ - url: `${TENANT_BASE_URL}/switch/${tenantId}`, + url: `${TENANT_BASE_URL}/${tenantId}/switch`, method: "post", }); }, diff --git a/src/components/TenantSelect/index.vue b/src/components/TenantSelect/index.vue index 42f774ef..12f55f17 100644 --- a/src/components/TenantSelect/index.vue +++ b/src/components/TenantSelect/index.vue @@ -35,7 +35,17 @@ const tenantStore = useTenantStoreHook(); // 当前租户名称 const currentTenantName = computed(() => { - return tenantStore.currentTenant?.name || "未选择租户"; + if (tenantStore.currentTenant?.name) { + return tenantStore.currentTenant.name; + } + // 如果当前租户信息不存在,尝试从租户列表中查找 + if (tenantStore.currentTenantId) { + const tenant = tenantStore.tenantList.find((t) => t.id === tenantStore.currentTenantId); + if (tenant) { + return tenant.name; + } + } + return "未选择租户"; }); // 当前租户ID @@ -74,30 +84,35 @@ onMounted(() => { .tenant-select { display: flex; align-items: center; - padding: 0 12px; + height: 100%; + padding: 0 8px; cursor: pointer; + border-radius: 4px; transition: all 0.3s; &__icon { margin-right: 6px; font-size: 18px; - color: var(--el-text-color-regular); + color: inherit; + transition: color 0.3s; } &__name { - max-width: 120px; + max-width: 100px; overflow: hidden; text-overflow: ellipsis; font-size: 14px; - color: var(--el-text-color-regular); + color: inherit; white-space: nowrap; + transition: color 0.3s; } &__arrow { margin-left: 6px; font-size: 12px; - color: var(--el-text-color-secondary); - transition: transform 0.3s; + color: inherit; + opacity: 0.7; + transition: all 0.3s; } &:hover { @@ -107,6 +122,11 @@ onMounted(() => { .tenant-select__name { color: var(--el-color-primary); } + + .tenant-select__arrow { + color: var(--el-color-primary); + opacity: 1; + } } } diff --git a/src/enums/api/code-enum.ts b/src/enums/api/code-enum.ts index 6afe5904..fd06b2c3 100644 --- a/src/enums/api/code-enum.ts +++ b/src/enums/api/code-enum.ts @@ -20,4 +20,9 @@ export const enum ApiCodeEnum { * 刷新令牌无效或过期 */ REFRESH_TOKEN_INVALID = "A0231", + + /** + * 需要选择租户 + */ + CHOOSE_TENANT = "A0250", } diff --git a/src/layouts/components/NavBar/components/NavbarActions.vue b/src/layouts/components/NavBar/components/NavbarActions.vue index 159b3808..890aedbd 100644 --- a/src/layouts/components/NavBar/components/NavbarActions.vue +++ b/src/layouts/components/NavBar/components/NavbarActions.vue @@ -100,9 +100,19 @@ const router = useRouter(); // 是否为桌面设备 const isDesktop = computed(() => appStore.device === DeviceEnum.DESKTOP); -// 是否显示租户选择(如果用户有多个租户或已选择租户) +// 是否显示租户选择(如果用户有多个租户,则显示租户选择器) +// 最小侵入:只有在多租户模式下(租户列表长度 > 1)才显示 const showTenantSelect = computed(() => { - return tenantStore.tenantList.length > 0 || tenantStore.currentTenantId !== null; + // 如果租户列表为空,不显示 + if (tenantStore.tenantList.length === 0) { + return false; + } + // 如果只有一个租户,也不显示(单租户模式,用户无感知) + if (tenantStore.tenantList.length === 1) { + return false; + } + // 多个租户时才显示 + return true; }); /** @@ -257,6 +267,16 @@ function handleSettingsClick() { .user-profile__name { color: rgba(255, 255, 255, 0.85); } + + // 租户选择器在白色文字模式下的样式 + :deep(.tenant-select) { + color: rgba(255, 255, 255, 0.85); + + &:hover { + color: #fff; + background: rgba(255, 255, 255, 0.1); + } + } } // 深色文字样式(用于浅色背景:明亮主题下的左侧布局) @@ -278,6 +298,16 @@ function handleSettingsClick() { .user-profile__name { color: var(--el-text-color-regular) !important; } + + // 租户选择器在深色文字模式下的样式 + :deep(.tenant-select) { + color: var(--el-text-color-regular) !important; + + &:hover { + color: var(--el-color-primary) !important; + background: rgba(0, 0, 0, 0.04); + } + } } // 确保下拉菜单中的图标不受影响 diff --git a/src/plugins/permission.ts b/src/plugins/permission.ts index c81379ff..fa9ad4f1 100644 --- a/src/plugins/permission.ts +++ b/src/plugins/permission.ts @@ -2,6 +2,7 @@ import type { RouteRecordRaw } from "vue-router"; import NProgress from "@/utils/nprogress"; import router from "@/router"; import { usePermissionStore, useUserStore } from "@/store"; +import { useTenantStoreHook } from "@/store/modules/tenant-store"; export function setupPermission() { const whiteList = ["/login"]; @@ -38,6 +39,12 @@ export function setupPermission() { await userStore.getUserInfo(); } + // 登录成功后,尝试获取租户列表和当前租户信息(如果启用多租户) + // 最小侵入:如果接口失败,不影响正常流程(可能是单租户模式) + const tenantStore = useTenantStoreHook(); + // 由 tenantStore 内部自行判断前端多租户开关(VITE_APP_TENANT_ENABLED) + await tenantStore.prepareTenantContextAfterLogin(); + const dynamicRoutes = await permissionStore.generateRoutes(); dynamicRoutes.forEach((route: RouteRecordRaw) => { router.addRoute(route); diff --git a/src/store/modules/tenant-store.ts b/src/store/modules/tenant-store.ts index d95f529e..a8a69db2 100644 --- a/src/store/modules/tenant-store.ts +++ b/src/store/modules/tenant-store.ts @@ -1,6 +1,9 @@ import { store } from "@/store"; import TenantAPI, { type TenantInfo } from "@/api/system/tenant-api"; +// 前端多租户开关;默认开启,若后端未启用多租户可在 .env 设置 VITE_APP_TENANT_ENABLED=false +const TENANT_ENABLED = import.meta.env.VITE_APP_TENANT_ENABLED !== "false"; + const TENANT_ID_KEY = "current_tenant_id"; const TENANT_INFO_KEY = "current_tenant_info"; @@ -52,6 +55,39 @@ export const useTenantStore = defineStore("tenant", () => { }); } + /** + * 登录后初始化租户:获取列表并尽量确定当前租户 + * - 忽略错误,以便单租户模式不受影响 + */ + // 登录后准备租户上下文:先取租户列表,再用后端返回的当前租户;若单租户则自动选中 + async function prepareTenantContextAfterLogin() { + if (!TENANT_ENABLED) { + return; + } + + try { + await fetchTenantList(); + + if (tenantList.value.length > 0 && !currentTenantId.value) { + try { + const currentTenantInfo = await TenantAPI.getCurrentTenant(); + if (currentTenantInfo) { + setCurrentTenant(currentTenantInfo); + } else if (tenantList.value.length === 1) { + setCurrentTenant(tenantList.value[0]); + } + } catch (error) { + if (tenantList.value.length === 1) { + setCurrentTenant(tenantList.value[0]); + } + console.debug("获取当前租户信息失败(可能是单租户模式):", error); + } + } + } catch (error) { + console.debug("获取租户列表失败(可能是单租户模式):", error); + } + } + /** * 设置当前租户 * @@ -120,6 +156,7 @@ export const useTenantStore = defineStore("tenant", () => { currentTenantId, currentTenant, tenantList, + prepareTenantContextAfterLogin, fetchTenantList, setCurrentTenant, switchTenant, diff --git a/src/store/modules/user-store.ts b/src/store/modules/user-store.ts index 410cfffa..2922f456 100644 --- a/src/store/modules/user-store.ts +++ b/src/store/modules/user-store.ts @@ -1,6 +1,6 @@ import { store } from "@/store"; -import AuthAPI, { type LoginFormData } from "@/api/auth-api"; +import AuthAPI, { type LoginRequest } from "@/api/auth-api"; import UserAPI, { type UserInfo } from "@/api/system/user-api"; import { AuthStorage } from "@/utils/auth"; @@ -18,16 +18,16 @@ export const useUserStore = defineStore("user", () => { /** * 登录 * - * @param {LoginFormData} + * @param {LoginRequest} * @returns */ - function login(LoginFormData: LoginFormData) { + function login(loginRequest: LoginRequest) { return new Promise((resolve, reject) => { - AuthAPI.login(LoginFormData) + AuthAPI.login(loginRequest) .then((data) => { const { accessToken, refreshToken } = data; // 保存记住我状态和token - rememberMe.value = LoginFormData.rememberMe; + rememberMe.value = loginRequest.rememberMe ?? false; AuthStorage.setTokens(accessToken, refreshToken, rememberMe.value); resolve(); }) diff --git a/src/utils/request.ts b/src/utils/request.ts index 29016498..ba95cb0e 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -34,6 +34,7 @@ httpRequest.interceptors.request.use( } // 添加租户ID到请求头(如果存在) + // 注意:只有在登录成功后,tenantStore 才会初始化,所以这里需要 try-catch try { const tenantStore = useTenantStoreHook(); const tenantId = tenantStore.currentTenantId; @@ -41,8 +42,8 @@ httpRequest.interceptors.request.use( config.headers["tenant-id"] = String(tenantId); } } catch (error) { - // 如果租户 store 未初始化,忽略错误 - console.debug("Tenant store not available:", error); + // 如果租户 store 未初始化(如登录前),忽略错误 + // 这是正常的,因为多租户功能是可选的,未启用时不会初始化 tenantStore } return config; @@ -70,6 +71,15 @@ httpRequest.interceptors.response.use( return data; } + // 特殊处理:需要选择租户(不显示错误提示,返回特殊对象供业务层处理) + if (code === ApiCodeEnum.CHOOSE_TENANT) { + return Promise.reject({ + code: ApiCodeEnum.CHOOSE_TENANT, + data, + msg, + }); + } + // 业务错误 ElMessage.error(msg || "系统出错"); return Promise.reject(new Error(msg || "Business Error")); diff --git a/src/views/login/components/Login.vue b/src/views/login/components/Login.vue index f5882ec5..97676454 100644 --- a/src/views/login/components/Login.vue +++ b/src/views/login/components/Login.vue @@ -87,7 +87,34 @@ - + +
+

检测到你的账号属于多个租户,请选择登录租户:

+ + + {{ tenant.name }} + + +
+ +