Files
userbase/src/App.vue
T
2026-09-07 09:32:38 +08:00

226 lines
7.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, onMounted, reactive, provide, computed } from "vue";
import layout from "./components/layout.vue";
import { CurrentRole, LayoutData } from "./types/Basics";
import { useRouter } from "vue-router";
import config from "@/assets/config/host.config";
import { useDefaultStore } from "@/store/index.js";
import Apis from '@/api/index'
import { alovaInstance } from "@/utils/alova/alova-iview";
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
import { UserRoleEnum } from "./types";
import { isRouteAllowed } from "@/router";
const store = useDefaultStore();
const router = useRouter();
const { $bus } = useCurrentInstance();
const refLayout = useTemplateRef('refLayout');
const isRouterAlive = ref(true),
isLogin = ref(false),
isNoAuthority = ref(false);
// 当前登录用户,供 resetMenuPath 事件复用
const currentUser = ref<any>(null);
const layoutData: LayoutData = reactive({
isAlone: false,
appName: null,
appList: [],
modList: [],
userInfo: null,
baseHost: null,
ssoHost: null,
});
const reload = () => {
isRouterAlive.value = false;
setTimeout(() => {
isRouterAlive.value = true;
}, 0);
};
provide("reload", reload);
const layoutInit = (info: any, modList: any[], appList: Array<any>, baseHost: string, ssoHost: string) => {
layoutData.appName = "系统管理";
layoutData.baseHost = baseHost;
layoutData.ssoHost = ssoHost;
let { id, name, profilePicture } = info;
layoutData.modList = modList || [];
// 有子菜单时不算「单模块独占」模式,需展示左侧菜单
const hasChildren = modList.some((m: any) => m.children && m.children.length > 0);
layoutData.isAlone = modList.length <= 1 && !hasChildren;
layoutData.userInfo = { id, name, avatar: profilePicture };
layoutData.appList = appList || [];
};
/**
* 角色判定(降级兜底):GetUserScope 失败时按 GetUserInfo 的 user.type 推断——
* type=1 → SuperAdmin,其余 → School。同时写入 currentRole 供下游
* Utils.getCurrentRoleInfo 与请求头注入读取(保持兼容;正常路径以 GetUserScope 为准)。
*/
const setCurrentRole = (user: any) => {
const userId = user?.id ?? null;
const roleId = user?.type ?? null;
const currentRole: CurrentRole = {
childId: null,
roleId,
schId: null,
userId,
};
localStorage.setItem("currentRole", encodeURI(JSON.stringify(currentRole)));
// 主题样式已本地化到 src/assets/styles/layout.css(含 default-theme / base.layout.beta / _iview_override),不再请求远程样式
if (user?.type === 1) {
store.setUserRole(UserRoleEnum.SuperAdmin);
} else {
// type=2 或其他 → School(默认)
store.setUserRole(UserRoleEnum.School);
}
};
onMounted(async () => {
const { baseHost, ssoHost } = config;
try {
var user: any = await alovaInstance.Request(Apis.Base.getUserInfo());
currentUser.value = user;
// 系统字典
await store.fetchSysDict();
// 角色判定:优先 GetUserScope(后端单点判定四级访问级别,前端不重复实现规则),
// 失败时降级走 setCurrentRole 的 type 推断
setCurrentRole(user);
try {
const scope: any = await alovaInstance.Request(Apis.Base.getUserScope());
if (scope && scope.level != null) {
store.setUserRole(scope.level);
store.setUserScope({
bureauGroupId: scope.bureauGroupId ?? null,
schoolGroupIds: scope.schoolGroupIds ?? null,
});
}
} catch (e) {
console.warn("获取访问范围失败,按用户类型降级判定角色", e);
}
// NONE 级:整体「暂无权限」,不加载菜单(接口层已被后端 NONE 拦截器 403 双挡)
store.setRoleLoaded(true);
if (store.userRole === UserRoleEnum.None) {
isNoAuthority.value = true;
layoutInit(user, [], [], baseHost, ssoHost);
isLogin.value = true;
return;
}
// 首次导航早于角色落定被守卫放行:此处复核当前路由(直接敲 URL 场景),越权重定向 /no-auth
if (!isRouteAllowed(router.currentRoute.value, store.userRole)) {
router.replace("/no-auth");
}
// 模块列表(按当前级别过滤菜单权限矩阵)
await store.fetchModules();
layoutInit(user, store.modules, store.modules, baseHost, ssoHost);
// 登录后直接跳转到左侧菜单第一个可用入口
// (顶层可能是单页,如「组,用户管理」;也可能是分组,取其首个子项)
// 仅在用户处于根路径时才跳转,避免刷新业务页(如 /app-manage/list)被强制拉回
const firstModule = store.modules[0];
const firstPage = firstModule?.children?.length
? firstModule.children[0].page
: firstModule?.page;
if (firstPage && router.currentRoute.value.path === "/") {
router.replace(firstPage);
}
isLogin.value = true;
} catch (error) {
console.log(error);
if (error != "redirect") Message.error("载入用户信息失败!");
}
});
const getRouterKey: (route: any) => string = route => {
let str = route?.name;
// if (str === "clocklist") str += "-" + route.params.id;
return str;
};
/**
* 需要被 keep-alive 缓存的组件名(按路由 meta.keepAlive 推导)。
* include 按组件 name 匹配,因此对应页面需 defineOptions({ name }) 与之相等。
*/
const cachedNames = computed(() =>
router.getRoutes().filter((r: any) => r.meta?.keepAlive).map((r: any) => r.name as string)
);
</script>
<template>
<layout ref="refLayout" v-if="isLogin" :layoutData="layoutData"></layout>
<div v-if="isLogin" :class="['base-wrap-main', layoutData.isAlone ? 'alone-page' : '']">
<template v-if="!isNoAuthority">
<router-view v-if="!!isRouterAlive" v-slot="{ Component, route }">
<transition name="fade" mode="out-in">
<keep-alive :include="cachedNames">
<component :is="Component" :key="getRouterKey(route)" />
</keep-alive>
</transition>
</router-view>
</template>
<div v-else class="no-authority-wrap">
<h3>暂无权限</h3>
<h6>如需查看,请联络管理员为您分配权限</h6>
</div>
</div>
</template>
<style lang="scss">
/* 开始过渡阶段,动画出去阶段 */
.fade-enter-active,
.fade-leave-active {
transition: all 1ms cubic-bezier(0, 0.99, 1, 0.03);
}
/* 进入开始 */
.fade-enter {
// transform: translateY(10px);
opacity: 0;
}
/* 出去终点 */
.fade-leave-active {
// transform: translateY(-10px);
// opacity: 0;
}
.back-enter-active,
.back-leave-active {
transition: all 1ms cubic-bezier(0, 0.99, 1, 0.03);
}
/* 进入开始 */
.back-enter {
display: none;
}
/* 出去终点 */
.back-leave-active {
display: none;
}
#app {
height: 100%;
}
.base-wrap-main .main-wrapper {
height: 100%;
}
</style>