feat: 接入 UserHome 前端业务代码(含真实 src/依赖,保留 projman 标准 CI)
deploy-userbase-front / build-and-deploy (push) Failing after 1m54s
build / build (push) Successful in 3m36s

This commit is contained in:
chenxing
2026-09-09 22:37:59 +08:00
parent a53ce07228
commit 4279f99be1
270 changed files with 12479 additions and 25715 deletions
+137 -157
View File
@@ -1,184 +1,170 @@
<script setup lang="ts">
import { ref, onMounted, reactive, provide, computed } from "vue";
import layout from "./components/layout.vue";
import { ref, onMounted, reactive, provide, computed, nextTick } from "vue";
import Basic from "@/api/BaseApi";
import { CurrentRole, LayoutData } from "./types/Basics";
import { useRouter } from "vue-router";
import config from "@/assets/config/host.config";
import { RouteRecordRaw, useRoute, useRouter } from "vue-router";
import ContextMenu from "@/components/ContextMenu.vue";
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";
import { Message } from "view-ui-plus";
import LeftMenu from "@/components/LeftMenu.vue";
import MyHeader from "@/components/Header.vue";
import BschIframe from "@/components/BschIframe.vue";
import SearchPanel from "@/components/SearchPanel.vue";
import UserInfoModal from "@/components/UserInfoModal.vue";
import LightPageModal from "@/components/LightPageModal.vue";
import BaseApi from "@/api/BaseApi";
import { BschIframeType, IframePageParmas, RoleInfo, StateEnum } from "./types";
import { storeToRefs } from "pinia";
import UserSettingApi from "./api/UserSettingApi";
import { useCompRef } from "./utils/hook/useCompRef";
import mittBus from "@/utils/mitt";
import MenuBarApi from "./api/MenuBarApi";
import { isTickeRoterByName } from "./utils/bschLinkPage";
import Utils from "./utils";
const store = useDefaultStore();
const route = useRoute();
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 isRouterAlive = ref(false),
StateType = ref<StateEnum>(StateEnum.Loading);
const reload = () => {
const refBschIframe = useCompRef(BschIframe);
const isBaseMask = computed(() => route.name !== "index");
const isHeader = computed(() => !(route.meta.isHeader === false));
mittBus.on("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("载入用户信息失败!");
}
}, 100);
});
mittBus.on("updateState", (state: StateEnum) => (StateType.value = state));
mittBus.on("switchRole", switchRole);
mittBus.on("reloadUserSettings", loadUserSettings);
mittBus.on("openBschIframePage", async (params: IframePageParmas) => {
StateType.value = StateEnum.Iframe;
await nextTick();
refBschIframe.value?.openPage(params);
});
(async () => {
console.time("init");
const userinfo = await BaseApi.getUserInfo();
const p1 = BaseApi.getUserRole();
const p2 = BaseApi.findUserRole();
try {
const [r1, r2] = await Promise.all([p1, p2]);
store.$state.userInfo = userinfo;
if (r1.length == 0) {
store.$state.userRoleList = [
{
roleId: 0,
roleName: "",
schId: 0,
schName: "",
childId: 0,
childName: "",
},
];
} else store.$state.userRoleList = r1.filter(x => [1, 2, 3, 4, 5, 7].includes(x.roleId));
store.setCurRoleInfo(r2 as RoleInfo);
// 已移除 bsch SSO ticket 获取(登录改走 CAS)
await switchRole();
} catch (error) {
Message.error("内部服务错误");
console.error(error);
return false;
}
isRouterAlive.value = true;
StateType.value = StateEnum.Desktop;
console.timeEnd("init");
})();
/**切换角色 */
async function switchRole() {
await loadUserMenuBarList();
await loadUserSettings();
store.curRoleUid = Utils.uuid();
// const p1 = BaseApi.getUsedApp(store.getCurRoleId, store.getCurSchId),
// p2 = BaseApi.getUserApp(store.getCurUserId, store.getCurSchId);
// const [r1, r2] = await Promise.all([p1, p2]);
// store.$state.curUsedAppList = r1;
// store.$state.curUserAppList = r2.filter(x => x.app.length > 0);
}
async function loadUserSettings() {
const userSettings = await UserSettingApi.getUserSettings();
store.setCurUserSettingsInfo(userSettings);
}
async function loadUserMenuBarList() {
const menuList = await MenuBarApi.getMenuBarUserList();
store.setCurUserMenuList(menuList);
let findIndex = -1;
if (isTickeRoterByName(route.name as string)) {
let menuId = route.params.menuId as string;
findIndex = menuList.findIndex(x => x.id == menuId);
} else {
findIndex = menuList.findIndex(x => x.url === route.path);
}
if (findIndex === -1) router.push({ name: "index" });
}
const getRouterKey: (route: any) => string = route => {
let str = route?.name;
// if (str === "clocklist") str += "-" + route.params.id;
return str;
};
const { menuVisible } = storeToRefs(store);
/**
* 需要被 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)
);
const mainStyle = computed(() => `background-image: url(${store.getCurWallPaperUrl})`);
</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">
<div
:class="['app-desktop app-desktop__animation', !menuVisible ? 'app-desktop__full' : '']"
:style="mainStyle"
v-show="StateType > StateEnum.Loading"
>
<LeftMenu />
<div :class="{ 'app-main-wrap': true, 'base-mask': isBaseMask }" v-if="StateType === StateEnum.Desktop">
<MyHeader v-if="isHeader" />
<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>
<template v-if="isBaseMask">
<component :is="Component" :key="route?.name" />
</template>
<transition v-else name="fade" mode="out-in">
<div class="app-main-wrap">
<component :is="Component" :key="route?.name" />
</div>
</transition>
</router-view>
</div>
<template v-if="StateType === StateEnum.Iframe">
<BschIframe ref="refBschIframe" />
</template>
<div v-else class="no-authority-wrap">
<h3>暂无权限</h3>
<h6>如需查看,请联络管理员为您分配权限</h6>
</div>
<div class="app-loading" v-if="StateType == StateEnum.Loading">
<div class="loadEffect">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
</div>
<!-- <SearchPanel class="app-search" v-show="StateType == StateEnum.Search" /> -->
<UserInfoModal />
<LightPageModal />
</template>
<style lang="scss">
@@ -187,13 +173,11 @@ const cachedNames = computed(() =>
.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);
@@ -204,21 +188,17 @@ const cachedNames = computed(() =>
.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%;
}