增加租户切换功能

This commit is contained in:
hero920103 committed 2026-09-29 15:38:00 +08:00
1 parent 1685b22e1e
commit 0dc5c2f075
8 files changed
+305 -60

No files matched your search

+3 -22
View File
@@ -69,9 +69,8 @@
</template>
</el-dropdown>
<span v-if="companyName" class="company-name" title="当前登录的企业">
{{ companyName }}
</span>
<!-- 当前企业 + 一人多企业时可点击切换 -->
<TenantSwitcher />
<el-dropdown trigger="click" @command="handleCommand">
<span class="el-dropdown-link" style="cursor: pointer;">
<img :src="getImageUrl('user')" class="user" />
@@ -120,6 +119,7 @@ import { User, SwitchButton, Sunny, Moon, Refresh, Bell, HomeFilled, Loading, Me
import { ElMessage } from 'element-plus';
import { getMySiteReminders, readAllSiteReminders } from "@/api/sitereminder";
import MessageDetailDialog from "./MessageDetailDialog.vue";
import TenantSwitcher from "./TenantSwitcher.vue";
import { formatTime } from "@/utils/datetime";
const router = useRouter();
@@ -290,11 +290,6 @@ const displayName = computed(() => {
return user.account || '';
});
// 当前登录的企业(租户)名称
const companyName = computed(() => {
return authStore.user?.tenant_name || '';
});
// 当前登录用户的角色 / 部门 / 职位(来自后端 /backend/getCurrentUserProfile)
const userProfile = reactive({ department: '', position: '', role_name: '' });
@@ -579,20 +574,6 @@ onUnmounted(() => {
}
}
.company-name {
max-width: 200px;
padding: 4px 12px;
font-size: 13px;
font-weight: 500;
color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
border: 1px solid var(--el-color-primary-light-7);
border-radius: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.el-dropdown-link {
display: flex;
align-items: center;
+221
View File
@@ -0,0 +1,221 @@
<template>
<!-- 当前企业 + 切换:企业名右侧带箭头,点击弹出该账号可进入的企业列表 -->
<el-dropdown
v-if="companyName"
trigger="click"
class="tenant-switcher"
popper-class="tenant-switcher-popper"
@command="handleSwitch"
>
<span class="tenant-chip" :class="[variantClass, 'is-switchable']" :title="`当前企业:${companyName},点击切换`">
<el-icon v-if="iconVisible" class="tenant-chip-icon"><OfficeBuilding /></el-icon>
<span class="tenant-chip-text">{{ companyName }}</span>
<el-icon class="tenant-chip-caret"><ArrowDown /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="item in tenantOptions"
:key="item.tid"
:command="item.tid"
:disabled="isCurrent(item.tid)"
>
<span class="tenant-option">
<span class="tenant-option-name">{{ item.tenant_name }}</span>
<el-icon v-if="isCurrent(item.tid)" class="tenant-option-check"><Check /></el-icon>
</span>
</el-dropdown-item>
<el-dropdown-item v-if="tenantOptions.length === 1" disabled>
<span class="tenant-option-tip">当前账号未关联其他企业</span>
</el-dropdown-item>
<el-dropdown-item v-if="!tenantOptions.length" :command="RELOAD_COMMAND" :disabled="loading">
<span class="tenant-option-tip">
{{ loading ? "企业列表加载中…" : "未获取到企业列表,点击重试" }}
</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { ElMessage } from "element-plus";
import { ArrowDown, Check, OfficeBuilding } from "@element-plus/icons-vue";
import { useAuthStore } from "@/stores/auth";
import { fetchUserInfo, resolveTenantName, switchTenant } from "@/utils/authClient";
/**
* 当前企业名 + 多企业切换。
*
* variant:
* chip —— 浅色标签样式(顶部栏使用)
* plain —— 无底色 + 楼宇图标(home 门户顶部使用)
*
* 只要拿到企业列表(≥1 家)就渲染「企业名 + 箭头」的可点击下拉,
* 只有一家时列表内给出说明;列表为空时退化为纯文本企业名。
*/
const props = withDefaults(defineProps<{ variant?: "chip" | "plain" }>(), {
variant: "chip",
});
const authStore = useAuthStore();
// 下拉里「重新拉取企业列表」的命令值
const RELOAD_COMMAND = "__reload__";
const tenantOptions = ref<Array<{ tid: number | string; tenant_name: string }>>([]);
const loading = ref(false);
const companyName = computed(() => authStore.user?.tenant_name || "");
const currentTid = computed(() => authStore.user?.tid || "");
const variantClass = computed(() => (props.variant === "plain" ? "is-plain" : "is-chip"));
const iconVisible = computed(() => props.variant === "plain");
function isCurrent(tid: number | string) {
return !!tid && String(tid) === String(currentTid.value);
}
async function loadTenants() {
if (!authStore.token) return;
loading.value = true;
try {
// 用认证中心 userinfo 取「可进入的企业」;同时兜底:本地缓存缺企业名时补上,
// 避免顶部企业名为空白(老会话、缓存被清等场景)
const info = await fetchUserInfo();
const list = Array.isArray(info?.tenants) ? info.tenants : [];
tenantOptions.value = list.map((t: any) => ({
tid: t.tid,
tenant_name: t.tenant_name || `企业${t.tid}`,
}));
const name = resolveTenantName(info);
if (name && !companyName.value) {
authStore.updateUserInfo({
...authStore.user,
tid: info?.tid ?? authStore.user?.tid,
tenant_name: name,
});
}
} catch (e) {
// 拉取失败不影响展示,下拉里会给出「点击重试」
tenantOptions.value = [];
console.error("加载可进入企业列表失败:", e);
} finally {
loading.value = false;
}
}
async function handleSwitch(tid: number | string) {
if (tid === RELOAD_COMMAND) {
await loadTenants();
return;
}
if (!tid || isCurrent(tid)) return;
const target = tenantOptions.value.find((item) => String(item.tid) === String(tid));
try {
await switchTenant(tid);
ElMessage.success(`已切换到「${target?.tenant_name || "新企业"}」`);
// 菜单、按钮权限、模块套餐均按企业隔离,整页重载确保全部按新企业重新初始化
setTimeout(() => window.location.reload(), 300);
} catch (e: any) {
ElMessage.error(e?.message || "切换企业失败");
}
}
onMounted(loadTenants);
</script>
<style scoped lang="less">
.tenant-chip {
display: inline-flex;
align-items: center;
gap: 4px;
max-width: 220px;
padding: 4px 12px;
font-size: 13px;
font-weight: 500;
color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
border: 1px solid var(--el-color-primary-light-7);
border-radius: 4px;
white-space: nowrap;
overflow: hidden;
&.is-switchable {
cursor: pointer;
transition: border-color 0.2s ease, background-color 0.2s ease;
&:hover {
border-color: var(--el-color-primary);
background-color: var(--el-color-primary-light-8);
}
}
.tenant-chip-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tenant-chip-caret {
flex: 0 0 auto;
font-size: 12px;
}
.tenant-chip-icon {
flex: 0 0 auto;
font-size: 15px;
}
}
// 顶部栏:与右侧圆形操作按钮等高对齐
.tenant-chip.is-chip {
height: 35px;
}
// home 门户顶部:无底色,图标 + 常规文字色(跟随主题变量自动适配暗色)
.tenant-chip.is-plain {
gap: 6px;
padding: 0;
max-width: 260px;
font-size: 15px;
color: var(--el-text-color-regular);
background-color: transparent;
border: none;
&.is-switchable:hover {
color: var(--el-color-primary);
background-color: transparent;
}
}
</style>
<!-- 下拉弹层由 el-dropdown 挂到 body,scoped 样式够不到,故用 popper 类名前缀限定 -->
<style lang="less">
.tenant-switcher-popper {
.tenant-option {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
min-width: 150px;
.tenant-option-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tenant-option-check {
flex: 0 0 auto;
color: var(--el-color-primary);
}
}
.tenant-option-tip {
font-size: 12px;
color: var(--el-text-color-secondary);
}
}
</style>