feat(site-layer): 学校站点分层方案C落地(docs/13)+栏目管理增强+管理端三修复+首页视觉升级管理端+审计复检T1~T5(docs/15)整合批次-URL站点段/site/{siteCode}无component镜像层(去name防重名,router-view跳过推进渲染)+身份条SiteSwitcher自绘(徽章#577afb/#ff9900+分组下拉✓+path次级列+Esc/外点收起)+URL唯一事实源syncSiteFromRoute(非法/跨校/主站码当子站段告警打回同页主站,localStorage降级刷新首帧兜底)+菜单剥段高亮stripSiteSeg/withSiteSeg补段+menuRoute镜像层防重影+跳转收敛adminPathOf(工作台进入/全部站点总览/站点管理行进入管理纯URL切站);栏目管理-上级栏目可选可改(禁选自身及后代防成环)+批量添加BatchAddCategory(行序写排序号单行失败不中断)+单页wangEditor富文本预览字数分区+categoryIds契约前端/types/mock三处统一+SchoolLayout高度链.layout-wrap修复+ArticleListPanel fixed模式;管理端三修复-皮肤预览Modal全屏(ThemePreviewFrame可选fill prop)+SiteManage补themeUnregistered声明+长图文编辑器随内容增高;管理端视觉升级配套-SiteConfigForm 12新键+BannerManage gallery展示位切换+栏目homeStyle下拉+PortalPreview侧栏同步;审计复检-新增siteRouter.spec真实路由集成7用例(镜像层matched3层/守卫子站码定向主站前缀/越权回工作台/旧链带id重定向)+sitePath空白容错+siteLayer持久化round-trip与身份条次级入口(docs/15);vitest 99/99+build通过+S1~S10 GUI走查全过
build / build (push) Successful in 2m43s
build / build (push) Successful in 2m43s
This commit is contained in:
+101
-25
@@ -1,14 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
/* 学校专属管理壳(docs/09 §3.3,由 layout.vue 改造,二级壳 /sch/:schCode):
|
||||
* header 左侧仅超管可见「↩返回学校列表」图标(校级账号不显示);
|
||||
* 面包屑首段=校名(落点为院系子站时后跟当前站点名),超管在校名前多一段可点「学校列表」(与返回图标同效)。
|
||||
* 学校/站点切换一律离壳完成:学校在 /workbench,站点在站点管理行「进入管理」。
|
||||
* 面包屑 = 学校列表(超管)> 校名 > 站点身份条 > 页面。
|
||||
* 站点分层(docs/13 方案 C):
|
||||
* URL 为站点上下文唯一事实源——主站 /sch/{code}/{page},子站 /sch/{code}/site/{siteCode}/{page};
|
||||
* 站点身份条(SiteSwitcher)一步切换并停留当前功能页,上下文由 syncSiteFromRoute 同步;
|
||||
* 菜单 page 保持主站形态,高亮剥段匹配、跳转按当前站点补段。
|
||||
* 学校切换仍在壳外完成:/workbench 工作台选校。
|
||||
* 菜单/高亮/子页面面包屑逻辑与原 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 { adminPathOf, stripSiteSeg, schoolSubPath, resolveRouteSite } from "@/utils/sitePath";
|
||||
import SiteSwitcher from "@/components/SiteSwitcher.vue";
|
||||
import config from "@/assets/config/host.config";
|
||||
|
||||
const route = useRoute();
|
||||
@@ -31,18 +37,21 @@ const modList = computed(() => store.session.modList || []);
|
||||
|
||||
const pathNavStyle = computed(() => (isAlone.value ? { left: "0", width: "100%" } : null));
|
||||
|
||||
/** 当前学校上下文(面包屑首段) */
|
||||
/** 当前学校上下文(面包屑校名段) */
|
||||
const school = computed(() => store.currentSchool);
|
||||
const isSuper = computed(() => store.isSuperAdmin);
|
||||
|
||||
/** 面包屑校名后缀:操作落点非主站时跟随展示当前站点名(院系子站管理可见性,
|
||||
* 站点切换入口在站点管理行「进入管理」,壳上不再放切换器) */
|
||||
const currentSiteSuffix = computed(() => {
|
||||
const s = store.currentSite;
|
||||
const sc = school.value;
|
||||
if (!s || !sc || s.id === sc.mainSiteId) return "";
|
||||
return s.name || "";
|
||||
});
|
||||
/** 当前落点是否院系子站(store 契约):决定身份条徽章与底边色带 */
|
||||
const isSubSite = computed(() => store.isSubSite);
|
||||
const pathNavClass = computed(() => (isSubSite.value ? "band-sub" : "band-main"));
|
||||
|
||||
/** 菜单/面包屑 page(主站形态完整路径)→ 当前站点形态:子站插入 /site/{code} 段(docs/13 §4)。
|
||||
* 非 /sch/ 前缀(外链模块)原样返回,防拼坏 */
|
||||
const withSiteSeg = (page: string): string => {
|
||||
const code = school.value?.mainSiteCode;
|
||||
if (!page || !code || !page.startsWith("/sch/")) return page;
|
||||
return adminPathOf(code, store.siteSegCode, schoolSubPath(page));
|
||||
};
|
||||
|
||||
const flaten = (tree: any[], pid = 0): any[] => {
|
||||
const arr: any[] = [];
|
||||
@@ -70,6 +79,8 @@ const routeChange = (to: any) => {
|
||||
const meta: any = to.meta || {};
|
||||
const { modId, title, subOf } = meta;
|
||||
const navArr: any[] = [];
|
||||
// 菜单 page 为主站形态,匹配前剥掉站点中缀(docs/13 §4 剥段匹配)
|
||||
const toLogic = stripSiteSeg(to.path || "");
|
||||
|
||||
// 侧边栏子菜单组自动展开:当前高亮落在某组子项时展开该组(改善直达体验)
|
||||
const expandGroupOf = (page: string) => {
|
||||
@@ -95,7 +106,7 @@ const routeChange = (to: any) => {
|
||||
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: parent.name, page: withSiteSeg(parent.page) });
|
||||
navArr.push({ name: title });
|
||||
navPath.value = navArr;
|
||||
return;
|
||||
@@ -105,7 +116,7 @@ const routeChange = (to: 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.page && x.page === toLogic) ||
|
||||
menuArr.value.find((x: any) => x.modId === modId);
|
||||
if (find) {
|
||||
currPath.value = find.page || "";
|
||||
@@ -114,7 +125,7 @@ const routeChange = (to: any) => {
|
||||
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 });
|
||||
navArr.push({ name: find.name || title, page: withSiteSeg(find.page) });
|
||||
navPath.value = navArr;
|
||||
return;
|
||||
}
|
||||
@@ -122,8 +133,8 @@ const routeChange = (to: any) => {
|
||||
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 }];
|
||||
currPath.value = stripSiteSeg(r.path);
|
||||
navPath.value = [{ name: (r.meta as any).title, page: withSiteSeg(r.path) }];
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -144,10 +155,12 @@ const linkMenu = (item: any) => {
|
||||
window.location.href = page;
|
||||
return;
|
||||
}
|
||||
if (page === route.path) {
|
||||
// 子站上下文下菜单目标补站点段(主站形态 → 当前站点形态)
|
||||
const target = withSiteSeg(page);
|
||||
if (target === route.path) {
|
||||
router.go(0);
|
||||
} else {
|
||||
router.push(page);
|
||||
router.push(target);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -192,7 +205,34 @@ function goWorkbench() {
|
||||
router.push("/workbench");
|
||||
}
|
||||
|
||||
/** 启动引导期守卫放行的深链:user 数据就绪后兜底校验/恢复学校上下文 */
|
||||
/**
|
||||
* URL 站点段 → store 同步(docs/13 §3:URL 是站点上下文唯一事实源)。
|
||||
* 无 siteCode = 主站形态;有则按 code 匹配本校站点(parentSiteId 归属校验),
|
||||
* 非法/跨校 siteCode 打回同页主站形态并告警。
|
||||
* 启动引导期(user 未装载 → schoolSites 为空)跳过,由 user 就绪后的 watch 兜底重跑。
|
||||
*/
|
||||
function syncSiteFromRoute() {
|
||||
const sc = school.value;
|
||||
if (!sc || !store.user) return;
|
||||
const sites = (store.schoolSites || []) as any[];
|
||||
if (!sites.length) return;
|
||||
const { site: target, invalid } = resolveRouteSite(sites, sc.mainSiteId, route.params.siteCode as string);
|
||||
if (invalid) {
|
||||
Message.warning("站点不存在或无权限,已返回主站");
|
||||
router.replace({ path: stripSiteSeg(route.path), query: route.query, hash: route.hash });
|
||||
return;
|
||||
}
|
||||
if (target && store.currentSite?.id !== target.id) {
|
||||
store.setCurrentSite({
|
||||
id: target.id,
|
||||
code: target.code,
|
||||
path: target.path || target.code,
|
||||
name: target.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** 启动引导期守卫放行的深链:user 数据就绪后兜底校验/恢复学校上下文,再按 URL 同步站点 */
|
||||
watch(
|
||||
() => store.user,
|
||||
u => {
|
||||
@@ -216,6 +256,8 @@ watch(
|
||||
sites.filter((s: any) => s.id === main.id || s.parentSiteId === main.id)
|
||||
);
|
||||
}
|
||||
syncSiteFromRoute();
|
||||
routeChange(route);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
@@ -226,13 +268,19 @@ onBeforeMount(() => {
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => routeChange(route)
|
||||
() => {
|
||||
syncSiteFromRoute();
|
||||
routeChange(route);
|
||||
}
|
||||
);
|
||||
|
||||
// 深链启动期:学校上下文/菜单晚于首次 routeChange 到达,就绪后重算面包屑与高亮
|
||||
watch(
|
||||
() => [store.currentSchool?.mainSiteCode, store.session.modList.length],
|
||||
() => routeChange(route)
|
||||
() => {
|
||||
syncSiteFromRoute();
|
||||
routeChange(route);
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -305,14 +353,18 @@ watch(
|
||||
<div class="clear-box"></div>
|
||||
</div>
|
||||
|
||||
<div class="path-nav" :style="pathNavStyle">
|
||||
<div class="path-nav" :class="pathNavClass" :style="pathNavStyle">
|
||||
<Breadcrumb separator=">">
|
||||
<!-- 超管:校名前多一段可点「学校列表」,与 header 返回图标同效(回工作台切换学校) -->
|
||||
<BreadcrumbItem v-if="isSuper" to="/workbench">学校列表</BreadcrumbItem>
|
||||
<!-- 面包屑首段 = 学校上下文(常驻);落点为院系子站时跟出当前站点名 -->
|
||||
<!-- 面包屑第二段 = 学校上下文(常驻,不承载站点信息) -->
|
||||
<BreadcrumbItem v-if="school">
|
||||
<Icon type="ios-school" />
|
||||
{{ school.schName || school.mainSiteName }}<template v-if="currentSiteSuffix"> · {{ currentSiteSuffix }}</template>
|
||||
{{ school.schName || school.mainSiteName }}
|
||||
</BreadcrumbItem>
|
||||
<!-- 站点身份条(docs/13 §1):徽章+站点名▼,下拉一步切换;不传 to(否则 mixins-link 抢走点击) -->
|
||||
<BreadcrumbItem v-if="store.currentSite">
|
||||
<SiteSwitcher />
|
||||
</BreadcrumbItem>
|
||||
<template v-for="(item, index) in navPath" :key="index">
|
||||
<BreadcrumbItem v-if="index != navPath.length - 1" :to="item.page">
|
||||
@@ -373,11 +425,35 @@ watch(
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 站点色带(docs/13 §1.4):::after 挂底边——.path-nav 高度 56px 固定且未设 border-box,
|
||||
直接加 border-bottom 会改盒高(W2 高度链教训),绝对定位不参与布局 */
|
||||
.path-nav::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 3px;
|
||||
}
|
||||
.path-nav.band-main::after {
|
||||
background: #577afb;
|
||||
}
|
||||
.path-nav.band-sub::after {
|
||||
background: #ff9900;
|
||||
}
|
||||
|
||||
/* W2(D6):左树高度链修复——UserBase App.vue:222-224 同款覆盖式。
|
||||
layout.css 全局 .main-wrapper 只有 min-height:100% 无 height,
|
||||
子级 .tree-wrapper{height:100%} 因父链高度 auto 解析失败,栏目树缩在一起。
|
||||
scoped 生效前提:.base-wrap-main/.main-wrapper 是本组件模板内节点(:348-349) */
|
||||
scoped 生效前提:.base-wrap-main/.main-wrapper 是本组件模板内节点 */
|
||||
.base-wrap-main .main-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 高度链顶层:layout.css 只定义到 .base-wrap-main{height:calc(100% - 56px)},
|
||||
其百分比基于 .layout-wrap——本组件根节点,无高度定义则整条链解析失败
|
||||
(UserBase 同名壳在 layout.vue 全局样式中补了这一层,此前 CMS 缺失) */
|
||||
.layout-wrap {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user