feat(aihr): 重构运营后台与租户知识治理

- 重构运营总览、侧栏和内容运营工作台\n- 增加大喇叭、成果审核与运营统计链路\n- 补齐租户、知识空间、分类和知识维护闭环\n- 加固标签页租户上下文、停用租户写保护与迁移预检
This commit is contained in:
2026-07-23 01:24:22 +08:00
parent 333c365c6f
commit 163b08ecfe
94 changed files with 8610 additions and 1114 deletions
+116 -28
View File
@@ -1,6 +1,12 @@
<template>
<div class="navbar" :class="'nav' + navType">
<hamburger id="hamburger-container" :is-active="appStore.sidebar.opened" class="hamburger-container" @toggle-click="toggleSideBar" />
<hamburger
v-if="navType !== NavTypeEnum.TOP"
id="hamburger-container"
:is-active="appStore.sidebar.opened"
class="hamburger-container"
@toggle-click="toggleSideBar"
/>
<breadcrumb v-if="navType == NavTypeEnum.LEFT" id="breadcrumb-container" class="breadcrumb-container" />
<top-nav v-if="navType == NavTypeEnum.MIX" id="topmenu-container" class="topmenu-container" />
@@ -11,12 +17,15 @@
</template>
<div class="right-menu flex align-center">
<template v-if="appStore.device !== 'mobile'">
<el-tag v-if="isSuperAdmin && tenantEnabled && !tenantContextVerified" class="mr-2" type="warning" effect="plain"> 租户上下文未确认 </el-tag>
<el-select
v-if="userId === 1 && tenantEnabled"
v-model="companyName"
v-if="isSuperAdmin && tenantEnabled"
v-model="selectedTenantId"
class="min-w-244px mr-2"
clearable
filterable
:disabled="tenantContextSwitching"
:loading="tenantContextSwitching"
reserve-keyword
:placeholder="proxy.$t('navbar.selectTenant')"
@change="dynamicTenantEvent"
@@ -34,7 +43,7 @@
</el-tooltip>
<!-- 消息 -->
<el-tooltip :content="proxy.$t('navbar.message')" effect="dark" placement="bottom">
<div style="display:flex;align-items:center">
<div style="display: flex; align-items: center">
<el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="300" :persistent="false">
<template #reference>
<el-badge :value="newNotice > 0 ? newNotice : ''" :max="99">
@@ -99,14 +108,28 @@ import { useUserStore } from '@/store/modules/user';
import { useSettingsStore } from '@/store/modules/settings';
import { useNoticeStore } from '@/store/modules/notice';
import { getTenantList } from '@/api/login';
import { dynamicClear, dynamicTenant } from '@/api/system/tenant';
import { getDynamicTenant } from '@/api/system/tenant';
import { TenantVO } from '@/api/types';
import notice from './notice/index.vue';
import router from '@/router';
import {
clearDynamicTenantContext,
clearDynamicTenantContextState,
dynamicTenantContextVersion,
dynamicTenantId,
hasDynamicTenant,
isTenantContextVersionCurrent,
markTenantContextUnverified,
restoreDynamicTenantContext,
switchDynamicTenantContext,
tenantContextVerified,
tenantContextSwitching,
waitForTenantRequestContextOwnership
} from '@/utils/tenant-context';
import { ElMessageBoxOptions } from 'element-plus/es/components/message-box/src/message-box.type';
import { NavTypeEnum } from '@/enums/NavTypeEnum';
import Logo from "@/layout/components/Sidebar/Logo.vue";
import TopBar from './TopBar'
import Logo from '@/layout/components/Sidebar/Logo.vue';
import TopBar from './TopBar';
const appStore = useAppStore();
const userStore = useUserStore();
@@ -116,14 +139,14 @@ const newNotice = ref(<number>0);
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const userId = ref(userStore.userId);
const navType = computed(() => settingsStore.navType);
const showLogo = computed(() => settingsStore.sidebarLogo);
const isSuperAdmin = computed(() => userStore.roles.includes('superadmin'));
const companyName = ref(undefined);
const selectedTenantId = ref<string | number>();
const tenantList = ref<TenantVO[]>([]);
// 是否切换了租户
const dynamic = ref(false);
const dynamic = hasDynamicTenant;
// 租户开关
const tenantEnabled = ref(true);
// 搜索菜单
@@ -134,33 +157,97 @@ const openSearchMenu = () => {
};
// 动态切换
const dynamicTenantEvent = async (tenantId: string) => {
if (companyName.value != null && companyName.value !== '') {
await dynamicTenant(tenantId);
dynamic.value = true;
await proxy?.$router.push('/');
await proxy?.$tab.closeAllPage();
await proxy?.$tab.refreshPage();
const dynamicTenantEvent = async (tenantId?: string | number) => {
if (tenantId === undefined || tenantId === null || tenantId === '' || tenantContextSwitching.value) return;
const tenant = tenantList.value.find((item) => String(item.tenantId) === String(tenantId));
if (!tenant) {
selectedTenantId.value = dynamicTenantId.value;
return;
}
try {
await switchDynamicTenantContext({ tenantId, companyName: tenant.companyName }, '/');
} catch {
selectedTenantId.value = dynamicTenantId.value;
}
};
const dynamicClearEvent = async () => {
await dynamicClear();
dynamic.value = false;
await proxy?.$router.push('/');
await proxy?.$tab.closeAllPage();
await proxy?.$tab.refreshPage();
if (tenantContextSwitching.value) return;
try {
await clearDynamicTenantContext('/');
} catch {
selectedTenantId.value = dynamicTenantId.value;
}
};
/** 租户列表 */
const initTenantList = async () => {
const { data } = await getTenantList(true);
tenantEnabled.value = data.tenantEnabled === undefined ? true : data.tenantEnabled;
if (tenantEnabled.value) {
tenantList.value = data.voList;
await waitForTenantRequestContextOwnership();
let contextVersion = dynamicTenantContextVersion.value;
const isCurrentInit = () => isTenantContextVersionCurrent(contextVersion);
let currentTenantId: string | number | null | undefined;
let dynamicContextLoaded = false;
// Read the page-scoped server context first. Without this echo, a restored
// dynamic tenant deliberately rejects the tenant list request as stale.
if (isSuperAdmin.value) {
try {
currentTenantId = (await getDynamicTenant()).data;
if (!isCurrentInit()) return;
restoreDynamicTenantContext(
currentTenantId !== undefined && currentTenantId !== null && currentTenantId !== '' ? { tenantId: currentTenantId } : undefined
);
contextVersion = dynamicTenantContextVersion.value;
dynamicContextLoaded = true;
} catch {
if (!isCurrentInit()) return;
}
}
try {
const { data } = await getTenantList(true);
if (!isCurrentInit()) return;
tenantEnabled.value = data.tenantEnabled === undefined ? true : data.tenantEnabled;
if (!tenantEnabled.value || !isSuperAdmin.value) {
tenantList.value = [];
clearDynamicTenantContextState();
return;
}
tenantList.value = data.voList.filter((tenant) => tenant.status === undefined || tenant.status === '0');
} catch {
if (!isCurrentInit()) return;
tenantList.value = [];
markTenantContextUnverified();
return;
}
if (!dynamicContextLoaded) {
markTenantContextUnverified();
return;
}
const tenant = tenantList.value.find((item) => String(item.tenantId) === String(currentTenantId));
if (currentTenantId !== undefined && currentTenantId !== null && currentTenantId !== '' && !tenant) {
markTenantContextUnverified();
return;
}
restoreDynamicTenantContext(
currentTenantId !== undefined && currentTenantId !== null && currentTenantId !== ''
? { tenantId: currentTenantId, companyName: tenant?.companyName }
: undefined
);
};
watch(
dynamicTenantId,
(tenantId) => {
selectedTenantId.value = tenantId;
},
{ immediate: true }
);
defineExpose({
initTenantList
});
@@ -282,7 +369,6 @@ watch(
margin-left: 8px;
}
.errLog-container {
display: inline-block;
vertical-align: top;
@@ -310,7 +396,9 @@ watch(
&.hover-effect {
cursor: pointer;
transition: background 0.2s ease, color 0.2s ease;
transition:
background 0.2s ease,
color 0.2s ease;
&:hover {
background: var(--el-fill-color-light);