- 同步仓库 CI 配置 .gitea/workflows/build.yml - 纳入 src/public/config/scripts 等工程文件 - .gitignore 增加 .scratch/.backup/.trash,排除本地临时产物
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onBeforeMount, onMounted, onUnmounted, watch, computed } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { resolveMenuPage, normalizeModList } from "@/utils/menuRoute";
|
||||
import { noticeApi } from "@/api/module/notice";
|
||||
|
||||
const props = defineProps<{ layoutData: Record<string, any> }>();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const isPanel = ref(false);
|
||||
const currPath = ref<string>("");
|
||||
const navPath = ref<any[]>([]);
|
||||
|
||||
const baseHost = computed(() => props.layoutData?.baseHost || "");
|
||||
const ssoHost = computed(() => props.layoutData?.ssoHost || "");
|
||||
const appName = computed(() => props.layoutData?.appName || "校园公告");
|
||||
const appList = computed(() => props.layoutData?.appList || []);
|
||||
const userInfo = computed(() => props.layoutData?.userInfo || { name: "管理员", avatar: "" });
|
||||
const isAlone = computed(() => props.layoutData?.isAlone ?? false);
|
||||
const modList = computed(() => props.layoutData?.modList || []);
|
||||
|
||||
const pathNavStyle = computed(() => (isAlone.value ? { left: "0", width: "100%" } : null));
|
||||
|
||||
/* 菜单管线(orderNumber 定序 → path 归一 → 草稿箱注入)已下沉 utils/menuRoute.normalizeModList,
|
||||
栏目名/顺序均以平台 GetUserMod 配置为准,前端不改名不重排(见执行计划任务方案 v10/v11) */
|
||||
|
||||
onBeforeMount(() => {
|
||||
routeChange(route);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => routeChange(route)
|
||||
);
|
||||
|
||||
const flaten = (tree: any[], pid = 0): any[] => {
|
||||
const arr: any[] = [];
|
||||
tree.forEach((element: any) => {
|
||||
const { id, name, modId } = element;
|
||||
// 平台配置的 path 格式不可控,统一解析为内部路由 path(名称/modId 兜底)
|
||||
const resolved = resolveMenuPage(element.page, element.modId ?? element.id, element.name);
|
||||
arr.push({ id, name, modId, page: resolved.path, external: resolved.external, pid });
|
||||
if (element.children && element.children.length) {
|
||||
arr.push(...flaten(element.children, id));
|
||||
}
|
||||
});
|
||||
return arr;
|
||||
};
|
||||
|
||||
const menuArr = computed(() => flaten(normalizeModList(modList.value)));
|
||||
const displayModList = computed(() => normalizeModList(modList.value));
|
||||
|
||||
const routeChange = (to: any) => {
|
||||
const meta: any = to.meta || {};
|
||||
const { modId, title, parentId } = meta;
|
||||
const navArr: any[] = [];
|
||||
|
||||
// 优先按 path 匹配菜单(平台模块 id 与路由 meta.modId 不保证一致),
|
||||
// 匹配不到再退回 modId 对照
|
||||
const find: any =
|
||||
menuArr.value.find((x: any) => x.page && x.page === to.path) ||
|
||||
menuArr.value.find((x: any) => x.modId === modId);
|
||||
if (find) {
|
||||
currPath.value = find.page || "";
|
||||
if (find.pid) {
|
||||
const pfind = menuArr.value.find((x: any) => x.id === find.pid);
|
||||
if (pfind) navArr.push({ name: pfind.name });
|
||||
}
|
||||
navArr.push({ name: find.name || title, page: find.page });
|
||||
navPath.value = navArr;
|
||||
return;
|
||||
}
|
||||
|
||||
if (parentId) {
|
||||
const parent = menuArr.value.find((x: any) => x.modId === parentId);
|
||||
if (parent) {
|
||||
currPath.value = parent.page || "";
|
||||
if (parent.pid) {
|
||||
const gp = menuArr.value.find((x: any) => 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 = [];
|
||||
};
|
||||
|
||||
const linkMenu = (item: any) => {
|
||||
const page: string = item?.page || "";
|
||||
if (!page) {
|
||||
// 解析不出有效页面:显式提示,而不是静默不动或被重定向吞到默认页
|
||||
Message.warning(`菜单「${item?.name || "未命名"}」未配置有效页面路径,请检查基础平台模块设置`);
|
||||
return;
|
||||
}
|
||||
// 平台配置的外部链接模块:整页跳转
|
||||
if (item?.external) {
|
||||
window.location.href = page;
|
||||
return;
|
||||
}
|
||||
if (page === route.path) {
|
||||
router.go(0);
|
||||
} else {
|
||||
router.push(page);
|
||||
}
|
||||
};
|
||||
|
||||
/* ============ 审批中心待审角标(ApprovalCount,60s 轮询 + 进页刷新) ============ */
|
||||
const approvalBadge = ref(0);
|
||||
let approvalTimer: number | undefined;
|
||||
|
||||
async function refreshApprovalBadge() {
|
||||
try {
|
||||
const n = await noticeApi.approvalCount().send();
|
||||
approvalBadge.value = Number(n) || 0;
|
||||
} catch {
|
||||
/* 未配置审批或接口异常时角标静默置 0 */
|
||||
approvalBadge.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refreshApprovalBadge();
|
||||
approvalTimer = window.setInterval(refreshApprovalBadge, 60000);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
if (approvalTimer) window.clearInterval(approvalTimer);
|
||||
});
|
||||
|
||||
const linkUserInfo = () => {
|
||||
window.location.href = baseHost.value + "/v/userinfo";
|
||||
};
|
||||
const logout = () => {
|
||||
let url = window.location.href;
|
||||
url = "/logout?service=" + url;
|
||||
window.location.href = ssoHost.value + url;
|
||||
};
|
||||
const childChange = (index: number) => {
|
||||
const arr = displayModList.value || [];
|
||||
if (arr[index]) {
|
||||
arr[index].childState = !arr[index].childState;
|
||||
}
|
||||
};
|
||||
|
||||
/** 根据菜单项映射图标:优先按解析后的 page 判断(平台模块 id 不可依赖),
|
||||
* 兜底保留旧版 modId 对照。图标为本地 bsch-icons(iconfont 掩码类,见 styles/icons.css) */
|
||||
const getMenuIcon = (item?: any): string => {
|
||||
const page: string = item?.page || "";
|
||||
if (page === "/list/new") return "icon-xiugaijl";
|
||||
if (page === "/drafts") return "icon-caogaoxiang";
|
||||
if (page === "/list") return "icon-fasong";
|
||||
if (page === "/" || page === "/accept") return "icon-sjx";
|
||||
if (page === "/noticelist") return "icon-liebiao";
|
||||
if (page === "/approval") return "icon-shenpi";
|
||||
if (page === "/stats") return "icon-tongji";
|
||||
if (page === "/setting") return "icon-shezhi";
|
||||
switch (item?.modId ?? item?.id) {
|
||||
case 1: return "icon-xiugaijl";
|
||||
case 2: return "icon-fasong";
|
||||
case 3: return "icon-sjx";
|
||||
case 4: return "icon-liebiao";
|
||||
case 5: return "icon-shenpi";
|
||||
case 6: return "icon-quanxian";
|
||||
case 21: return "icon-shezhi";
|
||||
case 22: return "icon-xiugaijl";
|
||||
default: return "";
|
||||
}
|
||||
};
|
||||
|
||||
function linkHomePage() {
|
||||
if (baseHost.value) {
|
||||
window.location.href = baseHost.value + "/BschHome/front/index.html";
|
||||
} else {
|
||||
window.location.href = "/";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout-wrap">
|
||||
<header class="header-container fix">
|
||||
<div class="fl align-middle icon-box">
|
||||
<div title="常用应用" :class="['icon-btn', isPanel ? 'active' : '']" @click="isPanel = !isPanel">
|
||||
<i class="iconfont icon-apps"></i>
|
||||
</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">
|
||||
<img :src="userInfo.avatar" class="fl align-middle" @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>
|
||||
</header>
|
||||
<div class="left-menu" v-if="!isAlone">
|
||||
<div class="menu-top-box"></div>
|
||||
<ul>
|
||||
<template v-for="(item, index) in displayModList" :key="'menu_li_' + index">
|
||||
<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>
|
||||
<i v-if="getMenuIcon(item)" :class="['iconfont', getMenuIcon(item)]"></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"
|
||||
:key="citem.id"
|
||||
>
|
||||
<a href="javascript:;" @click="linkMenu(citem)">
|
||||
{{ citem.name }}
|
||||
</a>
|
||||
</li>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
<li v-else :class="item.page == currPath ? 'active' : ''">
|
||||
<a href="javascript:;" @click="linkMenu(item)">
|
||||
<i v-if="getMenuIcon(item)" :class="['iconfont', getMenuIcon(item)]"></i>
|
||||
{{ item.name }}
|
||||
<span v-if="item.page === '/approval' && approvalBadge > 0" class="menu-badge">
|
||||
{{ approvalBadge > 99 ? "99+" : approvalBadge }}
|
||||
</span>
|
||||
</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="!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>
|
||||
|
||||
<transition name="fade_apps">
|
||||
<div class="app-panel" v-if="isPanel">
|
||||
<div class="tips-box"></div>
|
||||
<ul class="fix">
|
||||
<li class="fl" v-for="(item, index) in appList" :key="index">
|
||||
<a :href="item.url" target="_blank" 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 + '/BschHome/front/index.html#/apps'" target="_blank" rel="noopener noreferrer">
|
||||
全部应用
|
||||
<i>→</i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 仅覆盖/补充必要的动态样式,其余样式由全局 layout.css 提供 */
|
||||
/* 菜单图标(本地 bsch-icons 掩码类)与文字间距 */
|
||||
.left-menu a .iconfont {
|
||||
margin-right: 8px;
|
||||
}
|
||||
/* 审批中心待审角标 */
|
||||
.menu-badge {
|
||||
display: inline-block;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
padding: 0 5px;
|
||||
margin-left: 6px;
|
||||
border-radius: 9px;
|
||||
background: #ed4014;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user