build / build (push) Failing after 10s
- header-container 首位插入 .logo-badge(结构与 Notice 对齐,皮肤由通道 base.layout.css 提供);拷贝平台 yunhai-logo.svg 资产兜底 - localStorage["yh-logo-url"] 优先,@error 回落兜底;mounted 读一次 + 监听 yh-appearance-updated 即时切换 - sync 件:UserAvatar.vue 监听 yh-appearance-updated 重读键(YschTheme d9db725 分发)
307 lines
12 KiB
Vue
307 lines
12 KiB
Vue
<script setup lang="ts">
|
||
import { ref, inject, onBeforeMount, onBeforeUnmount, onMounted, watch, toRefs, nextTick, computed } from "vue";
|
||
import { LayoutData } from "@/types/Basics";
|
||
import { useRoute, useRouter } from "vue-router";
|
||
import { Message } from "view-ui-plus";
|
||
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
|
||
import SvgIcon from "@/components/SvgIcon.vue";
|
||
import UserAvatar from "@/components/UserAvatar.vue";
|
||
import defaultLogo from "@/assets/image/yunhai-logo.svg";
|
||
let props = defineProps<{ layoutData: LayoutData }>();
|
||
const reload: Function = inject("reload") || new Function("console.error('empty')");
|
||
|
||
const appPanel = ref();
|
||
let route = useRoute();
|
||
const router = useRouter();
|
||
|
||
// 顶栏 Logo:localStorage["yh-logo-url"](平台键,yh-theme-init 异步补齐段写入)优先,本仓资产兜底——绝无空 src/裂图。
|
||
// 刷新:mounted 读一次 + 监听 yh-appearance-updated(服务端偏好到达后即时切换,无需刷新页面)。
|
||
const logoUrl = ref<string>(defaultLogo);
|
||
const readLogoUrl = () => {
|
||
let v: string | null = null;
|
||
try {
|
||
v = localStorage.getItem("yh-logo-url");
|
||
} catch {
|
||
v = null;
|
||
}
|
||
logoUrl.value = v || defaultLogo;
|
||
};
|
||
const onLogoError = () => {
|
||
if (logoUrl.value !== defaultLogo) logoUrl.value = defaultLogo;
|
||
};
|
||
onMounted(() => {
|
||
readLogoUrl();
|
||
document.addEventListener("yh-appearance-updated", readLogoUrl);
|
||
});
|
||
onBeforeUnmount(() => document.removeEventListener("yh-appearance-updated", readLogoUrl));
|
||
|
||
const isPanel = ref(false),
|
||
currPath = ref<string>(""),
|
||
navPath = ref<any[]>([]);
|
||
const { baseHost, ssoHost, appName } = props.layoutData;
|
||
const { appList, userInfo, isAlone, modList } = toRefs(props.layoutData);
|
||
|
||
const modArr = computed(() => modList.value)
|
||
const { $bus } = useCurrentInstance();
|
||
|
||
const pathNavStyle = computed(() => (isAlone.value ? { left: "0", width: "100%" } : null));
|
||
|
||
onBeforeMount(async () => {
|
||
// 初次加载时手动同步一次(参考 bschstandard created 中手动触发 routeChange)
|
||
routeChange(route);
|
||
});
|
||
|
||
// 路由变化时实时同步左侧高亮与面包屑
|
||
watch(
|
||
() => route.fullPath,
|
||
() => routeChange(route)
|
||
);
|
||
|
||
/**
|
||
* 将菜单树扁平化(参考 bschstandard.frame.layout 的 flaten)
|
||
* - 子节点携带父节点的 id 作为 pid,用于面包屑与高亮联动
|
||
*/
|
||
const flaten = (tree: any[], pid = 0): any[] => {
|
||
const arr: any[] = [];
|
||
tree.forEach(element => {
|
||
const { id, name, page, modId } = element;
|
||
arr.push({ id, name, page, modId, pid });
|
||
if (element.children && element.children.length) {
|
||
arr.push(...flaten(element.children, id));
|
||
}
|
||
});
|
||
return arr;
|
||
};
|
||
|
||
const menuArr = computed(() => flaten(modList.value || []));
|
||
|
||
/**
|
||
* 同步当前路由对应的左侧高亮项与面包屑
|
||
* - ① 命中菜单树 modId:高亮自身;分组下的子项面包屑为「父级 / 自身」
|
||
* - ② 未命中但带 meta.parentId 的深层子页(如 应用类别管理):
|
||
* 用 parentId 找父菜单 → 高亮父菜单、面包屑为「(祖先/)父级(可点) / 当前页(不可点)」
|
||
* —— 对应效果「应用管理 > 应用类别管理」(参考 bschstandard.frame.layout 的 parentId 回退)
|
||
* - ③ 未在菜单树中的旧路由(/subject 等)降级按路由表兜底,仅展示自身
|
||
*/
|
||
const routeChange = (to: any) => {
|
||
const meta: any = to.meta || {};
|
||
const { modId, title, parentId } = meta;
|
||
const navArr: any[] = [];
|
||
|
||
// ① 命中菜单节点
|
||
const find: any = menuArr.value.find(x => x.modId === modId);
|
||
if (find) {
|
||
currPath.value = find.page || "";
|
||
if (find.pid) {
|
||
const pfind = menuArr.value.find(x => x.id === find.pid);
|
||
if (pfind) navArr.push({ name: pfind.name });
|
||
}
|
||
navArr.push({ name: find.name || title, page: find.page });
|
||
navPath.value = navArr;
|
||
return;
|
||
}
|
||
|
||
// ② 深层子页:按 meta.parentId 找父菜单,高亮父菜单并展开多级面包屑
|
||
if (parentId) {
|
||
const parent = menuArr.value.find(x => x.modId === parentId);
|
||
if (parent) {
|
||
currPath.value = parent.page || ""; // 高亮父菜单(如「应用管理」)
|
||
if (parent.pid) {
|
||
const gp = menuArr.value.find(x => x.id === parent.pid);
|
||
if (gp) navArr.push({ name: gp.name }); // 祖先(若父菜单本身在分组下)
|
||
}
|
||
navArr.push({ name: parent.name, page: parent.page }); // 父级可点击
|
||
navArr.push({ name: title }); // 当前深层页,不可点击
|
||
navPath.value = navArr;
|
||
return;
|
||
}
|
||
}
|
||
|
||
// ③ 旧路由兜底:路由表中带 modId 的,仅展示自身
|
||
const r = router.getRoutes().find(rt => (rt.meta as any)?.modId === modId);
|
||
if (r) {
|
||
currPath.value = r.path;
|
||
navPath.value = [{ name: (r.meta as any).title, page: r.path }];
|
||
return;
|
||
}
|
||
|
||
currPath.value = "";
|
||
navPath.value = [];
|
||
};
|
||
|
||
const showPanel = async () => {
|
||
if (appList.value.length == 0) {
|
||
Message.info("暂无常用应用!");
|
||
return;
|
||
}
|
||
isPanel.value = false;
|
||
await nextTick();
|
||
isPanel.value = true;
|
||
await nextTick();
|
||
|
||
var showPanelClick = () => {
|
||
appPanel.value.onclick = (e: any) => {
|
||
e.stopPropagation();
|
||
};
|
||
document.addEventListener(
|
||
"click",
|
||
() => {
|
||
isPanel.value = false;
|
||
},
|
||
{ once: true }
|
||
);
|
||
};
|
||
setTimeout(showPanelClick, 0);
|
||
};
|
||
|
||
const linkMenu = (page: string) => {
|
||
const path = route.path;
|
||
if (page === path) reload();
|
||
else router.push(page);
|
||
};
|
||
|
||
const linkUserInfo = () => {
|
||
// 头像/姓名点击统一跳 UserHome 设置页「个人信息」pane(默认头像轮 2026-09-30,原 /v/userinfo 已下线语义)
|
||
window.location.href = baseHost + "/userhome/front/index.html#/settings?pane=profile";
|
||
};
|
||
const logout = () => {
|
||
let url = window.location.href;
|
||
url = "/logout?service=" + url;
|
||
window.location.href = ssoHost + url;
|
||
};
|
||
const childChange = (index: number) => {
|
||
modArr.value[index].childState = !modArr.value[index].childState;
|
||
};
|
||
|
||
|
||
function linkHomePage() {
|
||
//@ts-ignore
|
||
const bschUtil = BschLoadUtil.getInstance();
|
||
if (!bschUtil.getIsSelf()) {
|
||
bschUtil.goBackDesktop();
|
||
} else {
|
||
// 旧 BschHome 已下线(线上 404),首页统一指向用户中心 userhome
|
||
window.location.href = baseHost + "/userhome/front/index.html";
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="layout-wrap">
|
||
<header class="header-container fix">
|
||
<!-- 云海 logo 徽标(结构与 Notice layout 对齐,皮肤见通道 base.layout.css .logo-badge) -->
|
||
<span class="logo-badge fl"><img :src="logoUrl" alt="云海" @error="onLogoError" /></span>
|
||
<div class="fl align-middle icon-box">
|
||
<div title="首页" class="icon-btn home-btn" @click="linkHomePage">
|
||
<SvgIcon name="shouye" :size="22" color="#fff" />
|
||
</div>
|
||
</div>
|
||
<div class="delimiter fl align-middle"></div>
|
||
<h4 class="fl align-middle">{{ appName }}</h4>
|
||
<div class="school-bg"></div>
|
||
<div class="header-aside fr">
|
||
<UserAvatar class="fl align-middle" :src="userInfo.avatar" :name="userInfo.name" :size="28" placement="brand" @click="linkUserInfo" />
|
||
<a class="name fl align-middle text-ellipsis" href="javascript:void(0);" @click="linkUserInfo">
|
||
{{ userInfo.name }}
|
||
</a>
|
||
<div class="delimiter fl align-middle"></div>
|
||
<a href="javascript:;" @click="logout" class="logout fl align-middle" title="退出登陆">
|
||
<SvgIcon name="logout" :size="18" color="#fff" />
|
||
</a>
|
||
</div>
|
||
</header>
|
||
<div class="left-menu" v-if="!isAlone">
|
||
<div class="menu-top-box"></div>
|
||
<ul>
|
||
<template v-for="(item, index) in modArr">
|
||
<template v-if="item.children && item.children.length > 0">
|
||
<li>
|
||
<a href="javascript:;" @click="childChange(index)">
|
||
<i :class="item.childState ? 'arrow-down' : 'arrow-up'"></i>
|
||
{{ item.name }}
|
||
|
||
</a>
|
||
</li>
|
||
|
||
<transition name="fade_child_ul">
|
||
<div class="child-item-wrap" v-show="item.childState">
|
||
<li :class="citem.page == currPath ? 'active child-item' : 'child-item'"
|
||
v-for="citem in item.children">
|
||
<a href="javascript:;" @click="linkMenu(citem.page)">
|
||
{{ citem.name }}
|
||
</a>
|
||
</li>
|
||
</div>
|
||
</transition>
|
||
</template>
|
||
<li v-else :class="item.page == currPath ? 'active' : ''" :key="'menu_li_' + index">
|
||
<a href="javascript:;" @click="linkMenu(item.page)">
|
||
{{ item.name }}
|
||
</a>
|
||
</li>
|
||
</template>
|
||
</ul>
|
||
<div class="clear-box"></div>
|
||
</div>
|
||
|
||
<div class="path-nav" :style="pathNavStyle">
|
||
<Breadcrumb separator=">">
|
||
<a href="javascript:;" @click="linkHomePage">
|
||
<BreadcrumbItem>
|
||
<i class="iconfont icon-category"></i>
|
||
首页
|
||
</BreadcrumbItem>
|
||
</a>
|
||
<BreadcrumbItem v-if="!layoutData.isAlone" to="/">{{ appName }}</BreadcrumbItem>
|
||
<template v-for="(item, index) in navPath">
|
||
<BreadcrumbItem v-if="index != navPath.length - 1" :to="item.page">
|
||
{{ item.name }}
|
||
</BreadcrumbItem>
|
||
<BreadcrumbItem v-else>{{ item.name }}</BreadcrumbItem>
|
||
</template>
|
||
</Breadcrumb>
|
||
</div>
|
||
|
||
<transition name="fade_apps">
|
||
<div class="app-panel" ref="appPanel" v-if="isPanel">
|
||
<div class="tips-box"></div>
|
||
<ul class="fix">
|
||
<li class="fl" v-for="item in appList">
|
||
<a :href="item.url" target="_bank" rel="noopener noreferrer">
|
||
<div class="icon-box">
|
||
<img :src="item.icon" alt="" />
|
||
</div>
|
||
<p class="text-ellipsis">{{ item.name }}</p>
|
||
</a>
|
||
</li>
|
||
</ul>
|
||
<div class="link-panel">
|
||
<a :href="baseHost + '/userhome/front/index.html#/apps'" target="_bank" rel="noopener noreferrer">
|
||
全部应用
|
||
<i>→</i>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</transition>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
/* 头部两个 ysch 单 svg 图标(SvgIcon):
|
||
layout.css 的 .header-aside .logout i / .icon-btn i 只命中 <i>,换 <svg> 后不再命中,
|
||
颜色与尺寸已由组件 props(#fff / 18、22)给出,此处只补布局——
|
||
<svg> 默认 inline 会带基线降部空隙,改 block 后与 Notice 的 mask 图标观感一致。
|
||
(Vue3 scoped 下子组件根节点会带父级 data-v,故 .logout svg 可命中) */
|
||
.header-aside .logout svg {
|
||
display: block;
|
||
}
|
||
.home-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.home-btn svg {
|
||
display: block;
|
||
}
|
||
</style>
|