Files
UserbaseFront/src/components/layout.vue
T
chenxing c12628f051
build / build (push) Failing after 10s
外观偏好异步补齐C:顶栏新增云海 logo 徽标 + 接平台键 yh-logo-url + sync 件更新
- 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 分发)
2026-10-08 18:52:31 +08:00

307 lines
12 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, 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>