Files
UserhomeFront/src/App.vue
T
chenxing e2c937f424
build / build (push) Successful in 5m35s
feat(avatar): 默认头像轮前端集成——设置页两级样式 + 登录写学校默认键 + 顶栏三态头像
- BaseApi:schAvatarStyle/saveSchAvatarStyle/clearSchAvatarStyle 三端点(后端 SchSettingsController 配套,本仓 service 仓随轮提交)
- App.vue:登录后 loadSchAvatarDefault()(与 loadSchThemeDefault 同构:失败/404/空→不写键不报错不阻塞),写 localStorage[yh-avatar-sch-default]
- Settings.vue:新增 avatar pane(我的头像样式人人可见 + 本校默认管理员可配,watch 进 pane 拉取)
- AppHeader/UserInfoModal:头像展示位换 UserAvatar 三态组件
- ThemePreview:预览页头像形态联动
运行时联调与部署验证待人工(后端端点须随 service 发版)
2026-10-06 20:20:44 +08:00

245 lines
8.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, nextTick } from "vue";
import Basic from "@/api/BaseApi";
import { RouteRecordRaw, useRoute, useRouter } from "vue-router";
import ContextMenu from "@/components/ContextMenu.vue";
import { useDefaultStore } from "@/store/index.js";
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 ThemePreview from "@/views/ThemePreview.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 isRouterAlive = ref(false),
StateType = ref<StateEnum>(StateEnum.Loading);
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;
}, 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;
loadSchThemeDefault();
loadSchAvatarDefault();
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");
})();
/** 拉学校默认主题档写键(异步尽力而为:失败/404/空 → 不写、不报错、不阻塞登录流程) */
function loadSchThemeDefault() {
BaseApi.getSchTheme()
.then((res: any) => {
if (res && res.themeState) {
try {
localStorage.setItem("yh-theme-sch-default", res.themeState);
} catch (e) {
/* 隐私模式写失败静默 */
}
}
})
.catch(() => undefined);
}
/** 拉学校默认头像样式写键(默认头像轮 2026-09-30;与 loadSchThemeDefault 同构:失败/404/空 → 不写、不报错、不阻塞登录流程) */
function loadSchAvatarDefault() {
BaseApi.getSchAvatarStyle()
.then((res: any) => {
if (res && res.avatarStyle) {
try {
localStorage.setItem("yh-avatar-sch-default", res.avatarStyle);
} catch (e) {
/* 隐私模式写失败静默 */
}
}
})
.catch(() => undefined);
}
/**切换角色 */
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);
}
// /msg、/settings 由后台「边栏设置」配置后即进入菜单表;未配置时也允许直达/刷新,不被弹回桌面。
// /theme-preview 同理(预览页按 URL 参数挂档,不依赖菜单表与登录态)
if (findIndex === -1 && !["/msg", "/settings", "/theme-preview"].includes(route.path)) 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);
const mainStyle = computed(() => `background-image: url(${store.getCurWallPaperUrl})`);
</script>
<template>
<!-- 主题预览页(主题设置 2026-09-30):顶层独立分支——不进桌面框架(无左栏/Header/壁纸)、
不依赖登录态与 isRouterAlive 门控(未登录直开也不白屏);仅在该 document 内动态引通道 init -->
<ThemePreview v-if="route.path === '/theme-preview'" />
<div
v-else
: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 }">
<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>
<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">
/* 开始过渡阶段,动画出去阶段 */
.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>