- A 类主题 loader 原文接入 + env-config 引导 + vite /static 与 /api(8086) 代理(P6/P11)
- GetUserMod 左导航:utils/menuNav.ts 单点实现 path 空锚点跳过、分组收敛、icon 兜底、面包屑基线(对齐 Notice 13afb22 refined 版)
- alova 封装 + api/module/base.ts(getUserMod 可选 appCode,P2 定名;H4 前空树不回落,P3)
- 路由骨架一 SPA 四形态:/admin/{forms,data,stats,dicts}、/f/:code、/app/:appCode/*、/my;self!=top 剥壳(方案 §3.3)
- 规范件落位:tableActions.ts(UserBase 同构源,正文 MD5 不变)、TablePageAlovaNew + useElementHeight、page-wrapper yh-fill 骨架
- view-ui-plus 精确锁 1.3.24;本地零 layout.css/icons.css;avatar 两件由 YschTheme sync 下发
- 剥除:notice 领域 views/api/mock/menu 兜底、alova-vant(mobiapi)、ztree+jquery 与重依赖(按需回补,见 service/docs/T5-report.md 问题单 P52)
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeMount, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { resolveMenuPage } from "@/utils/menuRoute";
|
||||
import { flattenNav, type NavNode } from "@/utils/menuNav";
|
||||
import UserAvatar from "@/components/UserAvatar.vue";
|
||||
import defaultLogo from "@/assets/image/yunhai-logo.svg";
|
||||
|
||||
const props = defineProps<{ layoutData: Record<string, any> }>();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const currPath = ref<string>("");
|
||||
const navPath = ref<Array<{ name: string; page?: string }>>([]);
|
||||
/** 分组展开态(按分组行 id 记忆,初始全折叠——与 Notice 现行行为一致) */
|
||||
const openGroups = ref<Record<number, boolean>>({});
|
||||
|
||||
const baseHost = computed(() => props.layoutData?.baseHost || "");
|
||||
const ssoHost = computed(() => props.layoutData?.ssoHost || "");
|
||||
const appName = computed(() => props.layoutData?.appName || "表单中心");
|
||||
const userInfo = computed(() => props.layoutData?.userInfo || { name: "用户", avatar: "" });
|
||||
const isAlone = computed(() => props.layoutData?.isAlone ?? false);
|
||||
const navList = computed<NavNode[]>(() => props.layoutData?.navList || []);
|
||||
|
||||
const pathNavStyle = computed(() => (isAlone.value ? { left: "0", width: "100%" } : null));
|
||||
|
||||
/** 扁平化导航项(高亮/面包屑对照表),由 utils/menuNav 的同一棵树导出,跳过规则只有一处实现 */
|
||||
const flatNav = computed(() => flattenNav(navList.value));
|
||||
|
||||
/**
|
||||
* 顶栏 Logo:localStorage["yh-logo-url"](平台键,yh-theme-init 异步补齐段写入)优先,
|
||||
* 本仓资产兜底——绝无空 src/裂图。刷新:mounted 读一次 + 监听 yh-appearance-updated(规范 §3)。
|
||||
*/
|
||||
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;
|
||||
};
|
||||
|
||||
onBeforeMount(() => routeChange(route));
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => routeChange(route)
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
readLogoUrl();
|
||||
document.addEventListener("yh-appearance-updated", readLogoUrl);
|
||||
});
|
||||
onBeforeUnmount(() => document.removeEventListener("yh-appearance-updated", readLogoUrl));
|
||||
|
||||
/** 面包屑/高亮:优先按 path 匹配导航项,匹配不到再退回 modId 对照 */
|
||||
const routeChange = (to: any) => {
|
||||
const meta: any = to.meta || {};
|
||||
const { modId, title, parentId } = meta;
|
||||
const navArr: Array<{ name: string; page?: string }> = [];
|
||||
const items = flatNav.value;
|
||||
|
||||
const find: any =
|
||||
items.find((x: any) => x.page && x.page === to.path) || items.find((x: any) => x.modId === modId);
|
||||
if (find) {
|
||||
currPath.value = find.page || "";
|
||||
if (find.pid) {
|
||||
const parent = navList.value.find((x) => x.id === find.pid);
|
||||
if (parent) navArr.push({ name: parent.name });
|
||||
}
|
||||
navArr.push({ name: find.name || title, page: find.page });
|
||||
navPath.value = navArr;
|
||||
return;
|
||||
}
|
||||
|
||||
if (parentId) {
|
||||
const parent = items.find((x: any) => x.modId === parentId);
|
||||
if (parent) {
|
||||
currPath.value = parent.page || "";
|
||||
if (parent.pid) {
|
||||
const gp = navList.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;
|
||||
}
|
||||
}
|
||||
|
||||
const r = router.getRoutes().find((rt: any) => (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 = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* 菜单图标:读用户中心模块管理配置的 YschIcons 图标名(GetUserMod 的 icon 字段,如 list → icon-list)。
|
||||
* icons.css(ysch-icons 掩码版,基类 iconfont,类名 icon-<名称>)由中央静态通道下发;
|
||||
* 平台未配置图标时统一兜底 icon-list(规范 §4 / Notice 13afb22:菜单项恒有图标,保整列缩进/行高一致)
|
||||
*/
|
||||
const getMenuIcon = (item?: any): string => `icon-${String(item?.icon || "").trim() || "list"}`;
|
||||
|
||||
/** 统一解析菜单项目标:模板高亮与点击都走这里,避免 raw page 与解析后 path 不一致 */
|
||||
const resolveItemPage = (item: NavNode) => resolveMenuPage(item.raw, item.modId, item.name);
|
||||
const pageOf = (item: NavNode): string => resolveItemPage(item).path;
|
||||
|
||||
const linkMenu = (item: NavNode) => {
|
||||
const { path, external } = resolveItemPage(item);
|
||||
if (!path) {
|
||||
// 解析不出有效页面:显式提示,而不是静默不动或被兜底路由吞到入口页
|
||||
Message.warning(`菜单「${item?.name || "未命名"}」未配置有效页面路径,请检查基础平台模块设置`);
|
||||
return;
|
||||
}
|
||||
if (external) {
|
||||
window.location.href = path;
|
||||
return;
|
||||
}
|
||||
if (path === route.path) router.go(0);
|
||||
else router.push(path);
|
||||
};
|
||||
|
||||
const childChange = (item: NavNode) => {
|
||||
openGroups.value = { ...openGroups.value, [item.id]: !openGroups.value[item.id] };
|
||||
};
|
||||
|
||||
const linkUserInfo = () => {
|
||||
// 头像/姓名统一跳 UserHome 设置页「个人信息」pane(Notice 口径,旧 /v/userinfo 已下线)
|
||||
window.location.href = baseHost.value + "/userhome/front/index.html#/settings?pane=profile";
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
const url = "/logout?service=" + window.location.href;
|
||||
window.location.href = ssoHost.value + url;
|
||||
};
|
||||
|
||||
function linkHomePage() {
|
||||
if (baseHost.value) {
|
||||
// 旧 BschHome 已下线(线上 404),首页统一指向用户中心 userhome
|
||||
window.location.href = baseHost.value + "/userhome/front/index.html";
|
||||
} else {
|
||||
window.location.href = "/";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout-wrap">
|
||||
<header class="header-container fix">
|
||||
<!-- 云海 logo 徽标(结构与通道 base.layout.css .logo-badge 对齐;子应用不展示 LOGO 文字,规范 §3 红线) -->
|
||||
<span class="logo-badge fl"><img :src="logoUrl" alt="云海" @error="onLogoError" /></span>
|
||||
<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="退出登陆">
|
||||
<i class="iconfont icon-logout"></i>
|
||||
</a>
|
||||
</div>
|
||||
<!-- 配色切换器不建(规范 §3 红线):主题设置入口已收敛 UserHome,本地切换会被服务端生效值打回 -->
|
||||
</header>
|
||||
|
||||
<div class="left-menu" v-if="!isAlone">
|
||||
<div class="menu-top-box"></div>
|
||||
<ul>
|
||||
<template v-for="item in navList" :key="'menu_li_' + item.id">
|
||||
<template v-if="item.group">
|
||||
<li>
|
||||
<a href="javascript:;" @click="childChange(item)">
|
||||
<i :class="openGroups[item.id] ? 'arrow-down' : 'arrow-up'"></i>
|
||||
<i :class="['iconfont', getMenuIcon(item)]"></i>
|
||||
{{ item.name }}
|
||||
</a>
|
||||
</li>
|
||||
<transition name="fade_child_ul">
|
||||
<div class="child-item-wrap" v-show="openGroups[item.id]">
|
||||
<li
|
||||
:class="pageOf(citem) == currPath ? 'active child-item' : 'child-item'"
|
||||
v-for="citem in item.children"
|
||||
:key="citem.id"
|
||||
>
|
||||
<a href="javascript:;" @click="linkMenu(citem)">
|
||||
{{ citem.name }}
|
||||
</a>
|
||||
</li>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
<li v-else :class="pageOf(item) == currPath ? 'active' : ''">
|
||||
<a href="javascript:;" @click="linkMenu(item)">
|
||||
<i :class="['iconfont', getMenuIcon(item)]"></i>
|
||||
{{ 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>
|
||||
<span class="crumb-home">
|
||||
<i class="iconfont icon-shouye"></i>
|
||||
首页
|
||||
</span>
|
||||
</BreadcrumbItem>
|
||||
</a>
|
||||
<BreadcrumbItem v-if="!isAlone" to="/">{{ appName }}</BreadcrumbItem>
|
||||
<template v-for="(item, index) in navPath" :key="index">
|
||||
<BreadcrumbItem v-if="index != navPath.length - 1" :to="item.page">
|
||||
{{ item.name }}
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbItem v-else>{{ item.name }}</BreadcrumbItem>
|
||||
</template>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 仅覆盖/补充必要的动态样式,其余由中央静态通道的 base.layout.css(骨架 + 皮肤)提供。
|
||||
本地零 layout.css/icons.css(规范 §2.2 回收步,V22) */
|
||||
/* 菜单图标(通道下发的 ysch-icons 掩码类)与文字间距 */
|
||||
.left-menu a .iconfont {
|
||||
margin-right: 8px;
|
||||
}
|
||||
/* 顶栏已由通道皮肤改为 flex 垂直居中,这里不再用 float 时代的 13px 顶偏移补偿,
|
||||
仅留右侧间距;徽标大小/圆底/阴影属皮肤 */
|
||||
.header-container .logo-badge {
|
||||
margin: 0 10px 0 0;
|
||||
}
|
||||
/* 面包屑「首页」与后续级目的文字对齐(Notice 13afb22 refined 版,规范 §4):
|
||||
此前用 inline-flex,行内级盒的基线取首项——16px 无字图标(基线=方块底缘)把整盒顶高约 4px,
|
||||
首页文字比相邻级目高。改回普通行内流:图标以 -0.125em 基线偏移做光学居中,
|
||||
与相邻级目共用同一行盒基线,天然对齐 */
|
||||
.crumb-home .iconfont {
|
||||
position: static;
|
||||
top: 0;
|
||||
margin-right: 4px;
|
||||
vertical-align: -0.125em;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user