init local code
This commit is contained in:
@@ -0,0 +1,443 @@
|
||||
<script setup lang="ts">
|
||||
/* 学校专属管理壳(docs/09 §3.3,由 layout.vue 改造,二级壳 /sch/:schCode):
|
||||
* header 左侧「←学校列表」+ 校名徽标/快速切换下拉;面包屑首段=校名;
|
||||
* 面包屑栏右侧「站点切换(校内主站+院系子站)+ 预览门户」承接院系子站维度。
|
||||
* 菜单/高亮/子页面面包屑逻辑与原 layout 一致,布局数据来自 store.session。 */
|
||||
import { ref, computed, watch, onBeforeMount } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { resolveMenuPage } from "@/utils/menuRoute";
|
||||
import { useCmsStore } from "@/store";
|
||||
import config from "@/assets/config/host.config";
|
||||
import { portalBase } from "@/api/module/cms";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const store = useCmsStore();
|
||||
|
||||
const isPanel = ref(false);
|
||||
const currPath = ref<string>("");
|
||||
const navPath = ref<any[]>([]);
|
||||
|
||||
const baseHost = computed(() => config.baseHost);
|
||||
const ssoHost = computed(() => config.ssoHost);
|
||||
const appName = computed(() => "云海CMS");
|
||||
const userInfo = computed(() => ({
|
||||
name: store.session.baseUserName || "管理员",
|
||||
avatar: store.session.baseUserAvatar,
|
||||
}));
|
||||
const isAlone = computed(() => store.session.isAlone);
|
||||
const modList = computed(() => store.session.modList || []);
|
||||
|
||||
const pathNavStyle = computed(() => (isAlone.value ? { left: "0", width: "100%" } : null));
|
||||
|
||||
/** 当前学校 + 校内站点(主站在前,切换下拉用) */
|
||||
const school = computed(() => store.currentSchool);
|
||||
const schoolSites = computed(() => store.schoolSites);
|
||||
/** 快速切换学校候选 = 可管理顶层站点(主站) */
|
||||
const schoolOptions = computed(() =>
|
||||
((store.user?.sites || []) as any[]).filter((s: any) => !s.parentSiteId)
|
||||
);
|
||||
|
||||
const flaten = (tree: any[], pid = 0): any[] => {
|
||||
const arr: any[] = [];
|
||||
tree.forEach((element: any) => {
|
||||
const { id, name, modId } = element;
|
||||
// 平台配置的 path 格式不可控,统一解析为内部路由 path(名称/modId 兜底)。
|
||||
// 解析结果回写原对象:模板高亮(citem.page == currPath)与 linkMenu 消费的是
|
||||
// 原始 children,旧扁平路由下 raw page 与解析结果恰好相等,学校前缀后必须显式回写
|
||||
const resolved = resolveMenuPage(element.page, element.modId ?? element.id, element.name);
|
||||
if (resolved.path) {
|
||||
element.page = resolved.path;
|
||||
}
|
||||
element.external = resolved.external;
|
||||
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(modList.value));
|
||||
|
||||
const routeChange = (to: any) => {
|
||||
const meta: any = to.meta || {};
|
||||
const { modId, title, subOf } = meta;
|
||||
const navArr: any[] = [];
|
||||
|
||||
// 侧边栏子菜单组自动展开:当前高亮落在某组子项时展开该组(改善直达体验)
|
||||
const expandGroupOf = (page: string) => {
|
||||
if (!page) return;
|
||||
modList.value.forEach((m: any) => {
|
||||
if (m.children?.some((c: any) => c.page === page)) {
|
||||
m.childState = true;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 列表子页面(docs/09 §3.2,如新增/编辑文章):面包屑挂到所属列表下、
|
||||
// 侧边栏高亮锁定列表项,优先级高于本页菜单匹配。
|
||||
// subOf = 所属列表的业务子路径(/article);菜单解析后带学校前缀,按后缀匹配,modId 兜底
|
||||
if (subOf) {
|
||||
const parent =
|
||||
menuArr.value.find((x: any) => x.page && (x.page === subOf || x.page.endsWith(subOf))) ||
|
||||
menuArr.value.find((x: any) => x.modId === subOf);
|
||||
if (parent) {
|
||||
currPath.value = parent.page || "";
|
||||
expandGroupOf(currPath.value);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 优先按 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 || "";
|
||||
expandGroupOf(currPath.value);
|
||||
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 (modId != null) {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
const linkUserInfo = () => {
|
||||
window.location.href = baseHost.value + "/v/userinfo";
|
||||
};
|
||||
const logout = () => {
|
||||
let url = window.location.href;
|
||||
url = "/cas/logout?service=" + url;
|
||||
window.location.href = ssoHost.value + url;
|
||||
};
|
||||
const childChange = (index: number) => {
|
||||
const arr = modList.value || [];
|
||||
if (arr[index]) {
|
||||
arr[index].childState = !arr[index].childState;
|
||||
}
|
||||
};
|
||||
|
||||
/** 根据菜单项映射图标:优先按解析后的 page 判断(平台模块 id 不可依赖),
|
||||
* 兜底保留 modId 对照 */
|
||||
const getMenuIcon = (item?: any): string => {
|
||||
const page: string = item?.page || "";
|
||||
if (page === "/article" || page.startsWith("/article")) return "ios-document";
|
||||
if (page === "/category") return "ios-list-box";
|
||||
if (page === "/banner") return "ios-images";
|
||||
if (page === "/link") return "ios-link";
|
||||
if (page === "/attachment") return "ios-folder-open";
|
||||
if (page === "/site") return "ios-globe";
|
||||
switch (item?.modId ?? item?.id) {
|
||||
case 1: return "ios-document";
|
||||
case 2: return "ios-list-box";
|
||||
case 3: return "ios-images";
|
||||
case 4: return "ios-link";
|
||||
case 5: return "ios-folder-open";
|
||||
case 6: return "ios-globe";
|
||||
default: return "";
|
||||
}
|
||||
};
|
||||
|
||||
/* ===== 学校上下文(docs/09 §3.3)===== */
|
||||
function goWorkbench() {
|
||||
router.push("/workbench");
|
||||
}
|
||||
|
||||
/** 快速切换学校:守卫负责恢复上下文,进入后落在文章列表 */
|
||||
function onSchoolChange(code: string) {
|
||||
if (!code || code === store.schoolCode) return;
|
||||
router.push(`/sch/${code}/article`);
|
||||
}
|
||||
|
||||
function onSiteChange(siteId: number) {
|
||||
const site = schoolSites.value.find((s: any) => s.id === siteId);
|
||||
if (site) {
|
||||
store.setCurrentSite({ id: site.id, code: site.code, name: site.name });
|
||||
}
|
||||
}
|
||||
|
||||
function openPortal() {
|
||||
if (!store.currentSite) return;
|
||||
if (config.isMock) {
|
||||
// mock 模式:SPA 内部门户预览(免依赖后端 Thymeleaf 门户)
|
||||
router.push(`/sch/${store.schoolCode}/portal-preview`);
|
||||
} else {
|
||||
window.open(`${portalBase(store.currentSite.code)}/`, "_blank");
|
||||
}
|
||||
}
|
||||
|
||||
/** 启动引导期守卫放行的深链:user 数据就绪后兜底校验/恢复学校上下文 */
|
||||
watch(
|
||||
() => store.user,
|
||||
u => {
|
||||
if (!u) return;
|
||||
const code = route.params.schCode as string;
|
||||
if (route.path.startsWith("/sch/") && !school.value) {
|
||||
const sites = u.sites || [];
|
||||
const main = sites.find((s: any) => s.code === code);
|
||||
if (!main) {
|
||||
router.replace("/workbench");
|
||||
return;
|
||||
}
|
||||
store.enterSchool(
|
||||
{
|
||||
schId: main.schId,
|
||||
schName: main.schName,
|
||||
mainSiteId: main.id,
|
||||
mainSiteCode: main.code,
|
||||
mainSiteName: main.name,
|
||||
},
|
||||
sites.filter((s: any) => s.id === main.id || s.parentSiteId === main.id)
|
||||
);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onBeforeMount(() => {
|
||||
routeChange(route);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => routeChange(route)
|
||||
);
|
||||
|
||||
// 深链启动期:学校上下文/菜单晚于首次 routeChange 到达,就绪后重算面包屑与高亮
|
||||
watch(
|
||||
() => [store.currentSchool?.mainSiteCode, store.session.modList.length],
|
||||
() => routeChange(route)
|
||||
);
|
||||
</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>
|
||||
<a class="fl align-middle back-workbench" href="javascript:void(0);" title="返回学校列表" @click="goWorkbench">
|
||||
<Icon type="ios-arrow-back" />
|
||||
学校列表
|
||||
</a>
|
||||
<div class="delimiter fl align-middle"></div>
|
||||
<h4 class="fl align-middle">{{ appName }}</h4>
|
||||
<div class="delimiter fl align-middle"></div>
|
||||
<!-- 校名徽标 + 快速切换(当前学校常驻 header) -->
|
||||
<div class="school-badge fl align-middle" v-if="school">
|
||||
<Icon type="ios-school" :size="16" />
|
||||
<Select
|
||||
size="small"
|
||||
:model-value="school.mainSiteCode"
|
||||
class="school-select"
|
||||
transfer
|
||||
@on-change="onSchoolChange"
|
||||
>
|
||||
<Option v-for="s in schoolOptions" :key="s.id" :value="s.code">
|
||||
{{ s.schName || s.name }}
|
||||
</Option>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="header-aside fr">
|
||||
<img :src="userInfo.avatar" v-if="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 modList" :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>
|
||||
<Icon v-if="getMenuIcon(item)" :type="getMenuIcon(item)" />
|
||||
{{ 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)">
|
||||
<Icon v-if="getMenuIcon(item)" :type="getMenuIcon(item)" />
|
||||
{{ item.name }}
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
<div class="clear-box"></div>
|
||||
</div>
|
||||
|
||||
<div class="path-nav" :style="pathNavStyle">
|
||||
<Breadcrumb separator=">">
|
||||
<!-- 面包屑首段 = 学校上下文(常驻) -->
|
||||
<BreadcrumbItem v-if="school">
|
||||
<Icon type="ios-school" />
|
||||
{{ school.schName || school.mainSiteName }}
|
||||
</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 class="cms-site-bar fr">
|
||||
<span class="site-label">站点:</span>
|
||||
<Select
|
||||
size="small"
|
||||
:model-value="store.currentSite?.id"
|
||||
:placeholder="schoolSites.length ? '选择站点' : '暂无可管理站点'"
|
||||
style="width: 200px"
|
||||
transfer
|
||||
@on-change="onSiteChange"
|
||||
>
|
||||
<Option v-for="opt in schoolSites" :key="opt.id" :value="opt.id">
|
||||
{{ opt.name }}{{ opt.parentSiteId ? "(院系)" : "" }}
|
||||
</Option>
|
||||
</Select>
|
||||
<Button size="small" type="primary" ghost style="margin-left: 10px" @click="openPortal">
|
||||
<Icon type="ios-eye" />
|
||||
预览门户
|
||||
</Button>
|
||||
</div>
|
||||
</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 []" :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 class="base-wrap-main" :class="{ 'alone-page': isAlone }">
|
||||
<div class="main-wrapper">
|
||||
<router-view v-if="store.currentSite" />
|
||||
<div v-else class="no-site-tip">当前学校暂无可管理站点,请联系平台管理员在学校工作台添加。</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 返回学校列表 + 校名徽标:header 常驻学校上下文(docs/09 §3.3) */
|
||||
.back-workbench {
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
&:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
.school-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
.school-select {
|
||||
width: 180px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
/* 站点切换条:挂在面包屑条右侧,沿用 path-nav 白底视觉 */
|
||||
.cms-site-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 56px;
|
||||
margin-right: 24px;
|
||||
|
||||
.site-label {
|
||||
color: #777;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
.no-site-tip {
|
||||
height: 60vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user