feat: 新增租户切换组件和css目录优化
This commit is contained in:
@@ -19,7 +19,5 @@ VITE_MOCK_DEV_SERVER=false
|
|||||||
# 多租户功能开关
|
# 多租户功能开关
|
||||||
# ============================================
|
# ============================================
|
||||||
# 是否启用多租户功能(默认:false)
|
# 是否启用多租户功能(默认:false)
|
||||||
# true: 启用多租户,显示租户切换器,发送 tenant-id 请求头
|
|
||||||
# false: 禁用多租户,隐藏租户相关UI,不发送 tenant-id 请求头
|
|
||||||
# 注意:前端开关需要与后端配置(youlai.tenant.enabled)保持一致
|
# 注意:前端开关需要与后端配置(youlai.tenant.enabled)保持一致
|
||||||
VITE_APP_TENANT_ENABLED=false
|
VITE_APP_TENANT_ENABLED=true
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import request from "@/utils/request";
|
import request from "@/utils/request";
|
||||||
|
import type { MenuTypeEnum } from "@/enums/business";
|
||||||
|
|
||||||
const MENU_BASE_URL = "/api/v1/menus";
|
const MENU_BASE_URL = "/api/v1/menus";
|
||||||
|
|
||||||
const MenuAPI = {
|
const MenuAPI = {
|
||||||
@@ -42,7 +44,6 @@ export interface MenuQuery {
|
|||||||
/** 搜索关键字 */
|
/** 搜索关键字 */
|
||||||
keywords?: string;
|
keywords?: string;
|
||||||
}
|
}
|
||||||
import type { MenuTypeEnum } from "@/enums/system/menu-enum";
|
|
||||||
export interface MenuVO {
|
export interface MenuVO {
|
||||||
/** 子菜单 */
|
/** 子菜单 */
|
||||||
children?: MenuVO[];
|
children?: MenuVO[];
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ComponentSize } from "@/enums/settings/layout-enum";
|
import { ComponentSize } from "@/enums/settings";
|
||||||
import { useAppStore } from "@/store/modules/app-store";
|
import { useAppStore } from "@/store/modules/app-store";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|||||||
35
src/components/TenantSwitcher/index.vue
Normal file
35
src/components/TenantSwitcher/index.vue
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<el-select
|
||||||
|
v-if="tenantList.length > 0"
|
||||||
|
v-model="currentTenantIdRef"
|
||||||
|
placeholder="选择租户"
|
||||||
|
style="width: 180px"
|
||||||
|
@change="onChange"
|
||||||
|
>
|
||||||
|
<el-option v-for="item in tenantList" :key="item.id" :label="item.name" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { useTenantStoreHook } from "@/store/modules/tenant-store";
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "change", tenantId: number): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const tenantStore = useTenantStoreHook();
|
||||||
|
|
||||||
|
const tenantList = computed(() => tenantStore.tenantList);
|
||||||
|
|
||||||
|
const currentTenantIdRef = computed<number | null>({
|
||||||
|
get: () => tenantStore.currentTenantId,
|
||||||
|
set: (val) => {
|
||||||
|
tenantStore.currentTenantId = val;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function onChange(tenantId: number) {
|
||||||
|
emit("change", tenantId);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { type RouteLocationNormalized } from "vue-router";
|
import { type RouteLocationNormalized } from "vue-router";
|
||||||
import { useSettingsStore, useTagsViewStore } from "@/store";
|
import { useSettingsStore, useTagsViewStore } from "@/store";
|
||||||
import variables from "@/styles/variables.scss";
|
import variables from "@/styles/variables.module.scss";
|
||||||
import Error404 from "@/views/error/404.vue";
|
import Error404 from "@/views/error/404.vue";
|
||||||
|
|
||||||
const { cachedViews } = toRefs(useTagsViewStore());
|
const { cachedViews } = toRefs(useTagsViewStore());
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import { SidebarColor } from "@/enums/settings";
|
|||||||
import { useSettingsStore, useAppStore } from "@/store";
|
import { useSettingsStore, useAppStore } from "@/store";
|
||||||
import { isExternal } from "@/utils/index";
|
import { isExternal } from "@/utils/index";
|
||||||
import MenuItem from "./components/MenuItem.vue";
|
import MenuItem from "./components/MenuItem.vue";
|
||||||
import variables from "@/styles/variables.scss";
|
import variables from "@/styles/variables.module.scss";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ defineOptions({
|
|||||||
|
|
||||||
import { LocationQueryRaw, RouteRecordRaw } from "vue-router";
|
import { LocationQueryRaw, RouteRecordRaw } from "vue-router";
|
||||||
import { usePermissionStore, useAppStore, useSettingsStore } from "@/store";
|
import { usePermissionStore, useAppStore, useSettingsStore } from "@/store";
|
||||||
import variables from "@/styles/variables.scss";
|
import variables from "@/styles/variables.module.scss";
|
||||||
import { SidebarColor } from "@/enums/settings";
|
import { SidebarColor } from "@/enums/settings";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
<!-- 租户选择(如果启用多租户) -->
|
<!-- 租户选择(如果启用多租户) -->
|
||||||
<div v-if="showTenantSelect" class="navbar-actions__item">
|
<div v-if="showTenantSelect" class="navbar-actions__item">
|
||||||
<TenantSelect />
|
<TenantSwitcher @change="handleTenantChange" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ import Fullscreen from "@/components/Fullscreen/index.vue";
|
|||||||
import SizeSelect from "@/components/SizeSelect/index.vue";
|
import SizeSelect from "@/components/SizeSelect/index.vue";
|
||||||
import LangSelect from "@/components/LangSelect/index.vue";
|
import LangSelect from "@/components/LangSelect/index.vue";
|
||||||
import Notification from "@/components/Notification/index.vue";
|
import Notification from "@/components/Notification/index.vue";
|
||||||
import TenantSelect from "@/components/TenantSelect/index.vue";
|
import TenantSwitcher from "@/components/TenantSwitcher/index.vue";
|
||||||
import { useTenantStoreHook } from "@/store/modules/tenant-store";
|
import { useTenantStoreHook } from "@/store/modules/tenant-store";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -113,6 +113,18 @@ const showTenantSelect = computed(() => {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function handleTenantChange(tenantId: number) {
|
||||||
|
tenantStore
|
||||||
|
.switchTenant(tenantId)
|
||||||
|
.then(() => {
|
||||||
|
ElMessage.success("切换租户成功");
|
||||||
|
window.location.reload();
|
||||||
|
})
|
||||||
|
.catch((error: any) => {
|
||||||
|
ElMessage.error(error?.message || "切换租户失败");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 打开个人中心页面
|
* 打开个人中心页面
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import LeftLayout from "@/layouts/modes/left/index.vue";
|
|||||||
import TopLayout from "@/layouts/modes/top/index.vue";
|
import TopLayout from "@/layouts/modes/top/index.vue";
|
||||||
import MixLayout from "@/layouts/modes/mix/index.vue";
|
import MixLayout from "@/layouts/modes/mix/index.vue";
|
||||||
import Settings from "./components/Settings/index.vue";
|
import Settings from "./components/Settings/index.vue";
|
||||||
import { LayoutMode } from "@/enums/settings/layout-enum";
|
import { LayoutMode } from "@/enums/settings";
|
||||||
import { defaultSettings } from "@/settings";
|
import { defaultSettings } from "@/settings";
|
||||||
|
|
||||||
const { currentLayout } = useLayout();
|
const { currentLayout } = useLayout();
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ import TagsView from "../../components/TagsView/index.vue";
|
|||||||
import AppMain from "../../components/AppMain/index.vue";
|
import AppMain from "../../components/AppMain/index.vue";
|
||||||
import MenuItem from "../../components/Menu/components/MenuItem.vue";
|
import MenuItem from "../../components/Menu/components/MenuItem.vue";
|
||||||
import Hamburger from "@/components/Hamburger/index.vue";
|
import Hamburger from "@/components/Hamburger/index.vue";
|
||||||
import variables from "@/styles/variables.scss";
|
import variables from "@/styles/variables.module.scss";
|
||||||
import { isExternal } from "@/utils/index";
|
import { isExternal } from "@/utils/index";
|
||||||
import { useAppStore, usePermissionStore } from "@/store";
|
import { useAppStore, usePermissionStore } from "@/store";
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import App from "./App.vue";
|
|||||||
// ===== 样式导入 =====
|
// ===== 样式导入 =====
|
||||||
import "element-plus/theme-chalk/dark/css-vars.css";
|
import "element-plus/theme-chalk/dark/css-vars.css";
|
||||||
import "vxe-table/lib/style.css";
|
import "vxe-table/lib/style.css";
|
||||||
import "@/styles/dark/css-vars.css";
|
|
||||||
import "@/styles/index.scss";
|
import "@/styles/index.scss";
|
||||||
import "uno.css";
|
import "uno.css";
|
||||||
import "animate.css";
|
import "animate.css";
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const defaultSettings: AppSettings = {
|
|||||||
size: ComponentSize.DEFAULT,
|
size: ComponentSize.DEFAULT,
|
||||||
// 语言
|
// 语言
|
||||||
language: LanguageEnum.ZH_CN,
|
language: LanguageEnum.ZH_CN,
|
||||||
// 主题颜色 - 修改此值时需同步修改 src/styles/variables.scss
|
// 主题颜色 - 修改此值时需同步修改 src/styles/element-plus-vars.scss
|
||||||
themeColor: "#4080FF",
|
themeColor: "#4080FF",
|
||||||
// 是否显示水印
|
// 是否显示水印
|
||||||
showWatermark: false,
|
showWatermark: false,
|
||||||
@@ -52,7 +52,7 @@ export const authConfig = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// 主题色预设 - 经典配色方案
|
// 主题色预设 - 经典配色方案
|
||||||
// 注意:修改默认主题色时,需要同步修改 src/styles/variables.scss 中的 primary.base 值
|
// 注意:修改默认主题色时,需要同步修改 src/styles/element-plus-vars.scss 中的 primary.base 值
|
||||||
export const themeColorPresets = [
|
export const themeColorPresets = [
|
||||||
"#4080FF", // Arco Design 蓝 - 现代感强
|
"#4080FF", // Arco Design 蓝 - 现代感强
|
||||||
"#1890FF", // Ant Design 蓝 - 经典商务
|
"#1890FF", // Ant Design 蓝 - 经典商务
|
||||||
|
|||||||
@@ -108,35 +108,38 @@ export const useTenantStore = defineStore("tenant", () => {
|
|||||||
*
|
*
|
||||||
* @param tenantId 目标租户ID
|
* @param tenantId 目标租户ID
|
||||||
*/
|
*/
|
||||||
function switchTenant(tenantId: number) {
|
async function switchTenant(tenantId: number): Promise<void> {
|
||||||
return new Promise<void>((resolve, reject) => {
|
try {
|
||||||
TenantAPI.switchTenant(tenantId)
|
// 调用后端切换接口
|
||||||
.then((tenantInfo) => {
|
const tenantInfo = await TenantAPI.switchTenant(tenantId);
|
||||||
// 后端返回切换后的租户信息
|
|
||||||
if (tenantInfo) {
|
// 后端返回切换后的租户信息
|
||||||
setCurrentTenant(tenantInfo);
|
if (tenantInfo) {
|
||||||
} else {
|
setCurrentTenant(tenantInfo);
|
||||||
// 如果后端未返回,从租户列表中找到对应的租户信息
|
} else {
|
||||||
const tenant = tenantList.value.find((t) => t.id === tenantId);
|
// 如果后端未返回,从租户列表中找到对应的租户信息
|
||||||
if (tenant) {
|
const tenant = tenantList.value.find((t) => t.id === tenantId);
|
||||||
setCurrentTenant(tenant);
|
if (tenant) {
|
||||||
|
setCurrentTenant(tenant);
|
||||||
|
} else {
|
||||||
|
// 如果列表中没有,重新获取租户信息
|
||||||
|
try {
|
||||||
|
const info = await TenantAPI.getCurrentTenant();
|
||||||
|
if (info) {
|
||||||
|
setCurrentTenant(info);
|
||||||
} else {
|
} else {
|
||||||
// 如果列表中没有,重新获取租户信息
|
throw new Error("无法获取租户信息");
|
||||||
TenantAPI.getCurrentTenant()
|
|
||||||
.then((info) => {
|
|
||||||
if (info) {
|
|
||||||
setCurrentTenant(info);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(console.error);
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取租户信息失败:", error);
|
||||||
|
throw new Error("切换租户后无法获取租户信息");
|
||||||
}
|
}
|
||||||
resolve();
|
}
|
||||||
})
|
}
|
||||||
.catch((error) => {
|
} catch (error) {
|
||||||
reject(error);
|
console.error("切换租户失败:", error);
|
||||||
});
|
throw error;
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -150,6 +153,13 @@ export const useTenantStore = defineStore("tenant", () => {
|
|||||||
localStorage.removeItem(STORAGE_KEYS.TENANT_INFO);
|
localStorage.removeItem(STORAGE_KEYS.TENANT_INFO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置租户列表
|
||||||
|
*/
|
||||||
|
function setTenantList(list: TenantInfo[]) {
|
||||||
|
tenantList.value = list || [];
|
||||||
|
}
|
||||||
|
|
||||||
// 恢复本地租户信息
|
// 恢复本地租户信息
|
||||||
restoreTenant();
|
restoreTenant();
|
||||||
|
|
||||||
@@ -159,6 +169,7 @@ export const useTenantStore = defineStore("tenant", () => {
|
|||||||
tenantList,
|
tenantList,
|
||||||
loadTenant,
|
loadTenant,
|
||||||
fetchTenantList,
|
fetchTenantList,
|
||||||
|
setTenantList,
|
||||||
setCurrentTenant,
|
setCurrentTenant,
|
||||||
switchTenant,
|
switchTenant,
|
||||||
clearTenant,
|
clearTenant,
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
/* 暗黑模式通过 CSS 自定义变量,官方链接:https://element-plus.org/zh-CN/guide/dark-mode.html#%E9%80%9A%E8%BF%87-css */
|
|
||||||
html.dark {
|
|
||||||
.el-table {
|
|
||||||
/* 自定义表格选中高亮时当前行的背景颜色 */
|
|
||||||
--el-table-current-row-bg-color: var(--el-fill-color-light);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
30
src/styles/element-plus-vars.scss
Normal file
30
src/styles/element-plus-vars.scss
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Element Plus 变量覆盖
|
||||||
|
*
|
||||||
|
* 此文件用于覆盖 Element Plus 的默认主题变量
|
||||||
|
* 需要在 element-plus.scss 中导入,而不是在 variables.scss 中
|
||||||
|
*/
|
||||||
|
@forward "element-plus/theme-chalk/src/common/var.scss" with (
|
||||||
|
$colors: (
|
||||||
|
"primary": (
|
||||||
|
// 默认主题色 - 修改此值时需同步修改 src/settings.ts 中的 themeColor
|
||||||
|
"base": #4080ff,
|
||||||
|
),
|
||||||
|
"success": (
|
||||||
|
"base": #23c343,
|
||||||
|
),
|
||||||
|
"warning": (
|
||||||
|
"base": #ff9a2e,
|
||||||
|
),
|
||||||
|
"danger": (
|
||||||
|
"base": #f76560,
|
||||||
|
),
|
||||||
|
"info": (
|
||||||
|
"base": #a9aeb8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
$bg-color: (
|
||||||
|
"page": #f5f8fd,
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Element Plus 变量覆盖(必须在最前面)
|
||||||
|
@use "./element-plus-vars";
|
||||||
|
|
||||||
$border: 1px solid var(--el-border-color-light);
|
$border: 1px solid var(--el-border-color-light);
|
||||||
|
|
||||||
/* el-dialog */
|
/* el-dialog */
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
// 基础变量与主题色
|
// 1. 基础重置(补充 UnoCSS 预设未覆盖的全局样式)
|
||||||
@use "./variables";
|
|
||||||
|
|
||||||
// 基础重置与组件细化样式
|
|
||||||
@use "./reset";
|
@use "./reset";
|
||||||
@use "./element-plus";
|
|
||||||
|
|
||||||
// Vxe Table 主题覆写(CSS 变量 + 自定义样式)
|
// 2. 项目自定义主题变量(CSS 变量 / SCSS 变量 / JS 导出)
|
||||||
|
@use "./variables" as *;
|
||||||
|
|
||||||
|
// 3. UI 框架适配:Element Plus & Vxe Table
|
||||||
|
@use "./element-plus";
|
||||||
@use "./vxe-table";
|
@use "./vxe-table";
|
||||||
|
|
||||||
// 业务通用样式
|
// 4. 业务通用样式
|
||||||
@use "./common";
|
@use "./common";
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// 全局基础重置:补充 UnoCSS 预设未覆盖的项目级样式
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -36,11 +38,6 @@ body {
|
|||||||
text-rendering: optimizelegibility;
|
text-rendering: optimizelegibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
img,
|
img,
|
||||||
svg {
|
svg {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|||||||
16
src/styles/variables.module.scss
Normal file
16
src/styles/variables.module.scss
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/* stylelint-disable property-no-unknown */
|
||||||
|
|
||||||
|
// 通过 SCSS 变量导出给 JS/TS 使用的模块文件
|
||||||
|
// 注意:依赖 src/styles/variables.scss 中定义的 SCSS 变量
|
||||||
|
|
||||||
|
:export {
|
||||||
|
sidebar-width: $sidebar-width;
|
||||||
|
navbar-height: $navbar-height;
|
||||||
|
tags-view-height: $tags-view-height;
|
||||||
|
menu-background: $menu-background;
|
||||||
|
menu-text: $menu-text;
|
||||||
|
menu-active-text: $menu-active-text;
|
||||||
|
menu-hover: $menu-hover;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* stylelint-enable property-no-unknown */
|
||||||
@@ -1,29 +1,9 @@
|
|||||||
@forward "element-plus/theme-chalk/src/common/var.scss" with (
|
/**
|
||||||
$colors: (
|
* 项目自定义主题变量(CSS 变量 / SCSS 变量 / JS 导出)
|
||||||
"primary": (
|
* 与 Element Plus 主题变量覆盖(element-plus-vars.scss)职责分离
|
||||||
// 默认主题色 - 修改此值时需同步修改 src/settings.ts 中的 themeColor
|
*
|
||||||
"base": #4080ff,
|
* 注意:此文件以下划线开头,是 Sass partial,不会被单独编译,只能被其他文件导入
|
||||||
),
|
*/
|
||||||
"success": (
|
|
||||||
"base": #23c343,
|
|
||||||
),
|
|
||||||
"warning": (
|
|
||||||
"base": #ff9a2e,
|
|
||||||
),
|
|
||||||
"danger": (
|
|
||||||
"base": #f76560,
|
|
||||||
),
|
|
||||||
"info": (
|
|
||||||
"base": #a9aeb8,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
$bg-color: (
|
|
||||||
"page": #f5f8fd,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
/** 全局SCSS变量 */
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--menu-background: #fff; // 菜单背景色
|
--menu-background: #fff; // 菜单背景色
|
||||||
@@ -56,6 +36,11 @@ html.dark {
|
|||||||
--sidebar-logo-background: rgb(0 0 0 / 20%);
|
--sidebar-logo-background: rgb(0 0 0 / 20%);
|
||||||
--sidebar-logo-text-color: #fff;
|
--sidebar-logo-text-color: #fff;
|
||||||
|
|
||||||
|
.el-table {
|
||||||
|
/* 自定义表格选中高亮时当前行的背景颜色(暗黑模式) */
|
||||||
|
--el-table-current-row-bg-color: var(--el-fill-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
/** WangEditor Dark */
|
/** WangEditor Dark */
|
||||||
/* Textarea - css vars */
|
/* Textarea - css vars */
|
||||||
--w-e-textarea-bg-color: var(--el-bg-color); /* 深色背景 */
|
--w-e-textarea-bg-color: var(--el-bg-color); /* 深色背景 */
|
||||||
@@ -92,7 +77,7 @@ $sidebar-width-collapsed: 54px; // 侧边栏收缩宽度
|
|||||||
$navbar-height: 50px; // 导航栏高度
|
$navbar-height: 50px; // 导航栏高度
|
||||||
$tags-view-height: 34px; // TagsView 高度
|
$tags-view-height: 34px; // TagsView 高度
|
||||||
|
|
||||||
/* 供 JS/TS 侧按需读取的变量导出(保持与原 module 一致) */
|
/* 供 JS/TS 侧按需读取的变量导出 */
|
||||||
/* stylelint-disable property-no-unknown */
|
/* stylelint-disable property-no-unknown */
|
||||||
:export {
|
:export {
|
||||||
sidebar-width: $sidebar-width;
|
sidebar-width: $sidebar-width;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { ApiCodeEnum } from "@/enums/api";
|
|||||||
import { AuthStorage, redirectToLogin } from "@/utils/auth";
|
import { AuthStorage, redirectToLogin } from "@/utils/auth";
|
||||||
import { useTokenRefresh } from "@/composables/auth/useTokenRefresh";
|
import { useTokenRefresh } from "@/composables/auth/useTokenRefresh";
|
||||||
import { authConfig } from "@/settings";
|
import { authConfig } from "@/settings";
|
||||||
import { useTenantStoreHook } from "@/store/modules/tenant-store";
|
|
||||||
|
|
||||||
// 初始化token刷新组合式函数
|
// 初始化token刷新组合式函数
|
||||||
const { refreshTokenAndRetry } = useTokenRefresh();
|
const { refreshTokenAndRetry } = useTokenRefresh();
|
||||||
@@ -20,7 +19,7 @@ const httpRequest = axios.create({
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 请求拦截器 - 添加 Authorization 头和租户ID
|
* 请求拦截器 - 添加 Authorization 头
|
||||||
*/
|
*/
|
||||||
httpRequest.interceptors.request.use(
|
httpRequest.interceptors.request.use(
|
||||||
(config: InternalAxiosRequestConfig) => {
|
(config: InternalAxiosRequestConfig) => {
|
||||||
@@ -33,19 +32,6 @@ httpRequest.interceptors.request.use(
|
|||||||
delete config.headers.Authorization;
|
delete config.headers.Authorization;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加租户ID到请求头(如果存在)
|
|
||||||
// 注意:只有在登录成功后,tenantStore 才会初始化,所以这里需要 try-catch
|
|
||||||
try {
|
|
||||||
const tenantStore = useTenantStoreHook();
|
|
||||||
const tenantId = tenantStore.currentTenantId;
|
|
||||||
if (tenantId) {
|
|
||||||
config.headers["tenant-id"] = String(tenantId);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// 如果租户 store 未初始化(如登录前),忽略错误
|
|
||||||
// 这是正常的,因为多租户功能是可选的,未启用时不会初始化 tenantStore
|
|
||||||
}
|
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
|
|||||||
@@ -97,16 +97,7 @@
|
|||||||
>
|
>
|
||||||
<div class="tenant-select-content">
|
<div class="tenant-select-content">
|
||||||
<p class="tenant-select-tip">检测到你的账号属于多个租户,请选择登录租户:</p>
|
<p class="tenant-select-tip">检测到你的账号属于多个租户,请选择登录租户:</p>
|
||||||
<el-radio-group v-model="selectedTenantId" class="tenant-radio-group">
|
<TenantSwitcher @change="(id: number) => (selectedTenantId = id)" />
|
||||||
<el-radio
|
|
||||||
v-for="tenant in pendingTenants"
|
|
||||||
:key="tenant.id"
|
|
||||||
:label="tenant.id"
|
|
||||||
class="tenant-radio"
|
|
||||||
>
|
|
||||||
{{ tenant.name }}
|
|
||||||
</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="tenantDialogVisible = false">取消</el-button>
|
<el-button @click="tenantDialogVisible = false">取消</el-button>
|
||||||
@@ -146,12 +137,15 @@ import AuthAPI from "@/api/auth";
|
|||||||
import type { LoginRequest } from "@/types/api";
|
import type { LoginRequest } from "@/types/api";
|
||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
import { useUserStore } from "@/store";
|
import { useUserStore } from "@/store";
|
||||||
|
import { useTenantStoreHook } from "@/store/modules/tenant-store";
|
||||||
import CommonWrapper from "@/components/CommonWrapper/index.vue";
|
import CommonWrapper from "@/components/CommonWrapper/index.vue";
|
||||||
|
import TenantSwitcher from "@/components/TenantSwitcher/index.vue";
|
||||||
import { AuthStorage } from "@/utils/auth";
|
import { AuthStorage } from "@/utils/auth";
|
||||||
import { ApiCodeEnum } from "@/enums";
|
import { ApiCodeEnum } from "@/enums";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
const tenantStore = useTenantStoreHook();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
onMounted(() => getCaptcha());
|
onMounted(() => getCaptcha());
|
||||||
@@ -219,9 +213,6 @@ function getCaptcha() {
|
|||||||
.finally(() => (codeLoading.value = false));
|
.finally(() => (codeLoading.value = false));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 待选择的租户列表
|
|
||||||
const pendingTenants = ref<Array<{ id: number; name: string }>>([]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录提交
|
* 登录提交
|
||||||
*/
|
*/
|
||||||
@@ -243,7 +234,8 @@ async function handleLoginSubmit() {
|
|||||||
// 检查是否是 choose_tenant 响应
|
// 检查是否是 choose_tenant 响应
|
||||||
if (error?.code === ApiCodeEnum.CHOOSE_TENANT && error?.data?.tenants) {
|
if (error?.code === ApiCodeEnum.CHOOSE_TENANT && error?.data?.tenants) {
|
||||||
// 需要选择租户
|
// 需要选择租户
|
||||||
pendingTenants.value = error.data.tenants;
|
tenantStore.setTenantList(error.data.tenants);
|
||||||
|
selectedTenantId.value = error.data.tenants[0]?.id || null;
|
||||||
tenantDialogVisible.value = true;
|
tenantDialogVisible.value = true;
|
||||||
return; // 等待用户选择租户
|
return; // 等待用户选择租户
|
||||||
}
|
}
|
||||||
@@ -304,12 +296,12 @@ function toOtherForm(type: "register" | "resetPwd") {
|
|||||||
.auth-panel-form {
|
.auth-panel-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-panel-form__title {
|
.auth-panel-form__title {
|
||||||
margin: 0 0 0.75rem;
|
margin: 0 0 0.5rem;
|
||||||
font-size: 1.25rem;
|
font-size: 1.125rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,7 +309,7 @@ function toOtherForm(type: "register" | "resetPwd") {
|
|||||||
.divider-container {
|
.divider-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 24px 0;
|
margin: 16px 0;
|
||||||
|
|
||||||
.divider-line {
|
.divider-line {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -342,33 +334,5 @@ function toOtherForm(type: "register" | "resetPwd") {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--el-text-color-regular);
|
color: var(--el-text-color-regular);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tenant-radio-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.tenant-radio {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border: 1px solid var(--el-border-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
transition: all 0.3s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--el-color-primary-light-9);
|
|
||||||
border-color: var(--el-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-radio__input.is-checked) {
|
|
||||||
+ .el-radio__label {
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--el-color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -123,9 +123,7 @@ onBeforeUnmount(() => {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
padding: clamp(1rem, 3vw, 2rem);
|
padding: clamp(1rem, 3vw, 2rem);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background:
|
background-color: #f5f7ff;
|
||||||
radial-gradient(circle at 20% 20%, rgba(64, 128, 255, 0.18), transparent 55%),
|
|
||||||
radial-gradient(circle at 80% 80%, rgba(22, 93, 255, 0.16), transparent 50%);
|
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -201,13 +199,11 @@ onBeforeUnmount(() => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: clamp(1.5rem, 3vw, 3rem);
|
padding: clamp(1.5rem, 3vw, 3rem);
|
||||||
color: rgba(20, 40, 80, 0.95);
|
color: var(--el-text-color-primary);
|
||||||
text-shadow: 0 4px 16px rgba(15, 60, 110, 0.12);
|
|
||||||
animation: featureFade 0.8s ease-out;
|
animation: featureFade 0.8s ease-out;
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
color: rgba(236, 242, 255, 0.92);
|
color: rgba(236, 242, 255, 0.92);
|
||||||
text-shadow: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +268,7 @@ onBeforeUnmount(() => {
|
|||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
color: rgba(35, 40, 65, 0.85);
|
color: var(--el-text-color-regular);
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
color: rgba(220, 230, 255, 0.75);
|
color: rgba(220, 230, 255, 0.75);
|
||||||
@@ -292,8 +288,8 @@ onBeforeUnmount(() => {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: rgba(32, 37, 60, 0.9);
|
color: var(--el-text-color-primary);
|
||||||
background: rgba(255, 255, 255, 0.55);
|
background: rgba(255, 255, 255, 0.9);
|
||||||
border: 1px solid rgba(64, 128, 255, 0.08);
|
border: 1px solid rgba(64, 128, 255, 0.08);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
backdrop-filter: blur(6px);
|
backdrop-filter: blur(6px);
|
||||||
@@ -321,11 +317,12 @@ onBeforeUnmount(() => {
|
|||||||
.auth-panel {
|
.auth-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1.5rem;
|
gap: 1rem;
|
||||||
|
align-self: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
justify-self: end;
|
justify-self: end;
|
||||||
width: min(520px, 100%);
|
width: min(420px, 100%);
|
||||||
padding: clamp(2rem, 3vw, 2.75rem);
|
padding: clamp(1.5rem, 3vw, 2rem);
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
background: rgba(255, 255, 255, 0.95);
|
background: rgba(255, 255, 255, 0.95);
|
||||||
border: 1px solid rgba(22, 93, 255, 0.1);
|
border: 1px solid rgba(22, 93, 255, 0.1);
|
||||||
@@ -359,11 +356,11 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
.auth-panel__brand {
|
.auth-panel__brand {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 0.75rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding-bottom: 1.25rem;
|
padding-bottom: 0.875rem;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1rem;
|
||||||
border-bottom: 1px solid rgba(22, 93, 255, 0.06);
|
border-bottom: 1px solid rgba(22, 93, 255, 0.06);
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
@@ -449,7 +446,7 @@ onBeforeUnmount(() => {
|
|||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
|
|
||||||
:deep(.el-form-item) {
|
:deep(.el-form-item) {
|
||||||
margin-bottom: 1.25rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-input__wrapper) {
|
:deep(.el-input__wrapper) {
|
||||||
@@ -472,8 +469,8 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.auth-panel__footer {
|
.auth-panel__footer {
|
||||||
padding-top: 1.25rem;
|
padding-top: 0.875rem;
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.125rem;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-top: 1px solid rgba(22, 93, 255, 0.06);
|
border-top: 1px solid rgba(22, 93, 255, 0.06);
|
||||||
|
|||||||
Reference in New Issue
Block a user