feat(site-layer): S8 站点上下文卡 + 子站身份染色(方案 A,零后端改动)
- 新建 components/SiteContextCard.vue(194):数据源 store.schoolSites(含 SiteVo.theme), 展示学校›站点层级与当前站皮肤包名;切站沿用既有纯 URL 通道 adminPathOf,不新增状态机 - SchoolLayout.vue 挂载卡片到原空 .menu-top-box 槽 + 子站态身份染色(侧栏淡底 #fff7ec、 卡区顶 3px 橙条、页头胶囊转橙);主站态不染色 - 硬约束:未动 utils/sitePath.ts 的 stripSiteSeg/withSiteSeg(剥段高亮全靠它)、未动 router/routes.ts - siteLayer.spec.ts +4 例(纯加,SITES 夹具补 theme 字段);护栏 siteRouter.spec(8)/sitePath.spec(14)/ menuRoute.spec(5) 原样绿 - 遗留立案:R-39 isAlone 档不可见 / R-40 门户地址未展示 / R-41 面包屑是否退役 SiteSwitcher 待裁决 / R-42 显示包 code 而非中文名 - 前端 164/164 绿 + build:base exit=0
This commit is contained in:
@@ -15,6 +15,7 @@ import { resolveMenuPage } from "@/utils/menuRoute";
|
||||
import { useCmsStore } from "@/store";
|
||||
import { adminPathOf, stripSiteSeg, schoolSubPath, resolveRouteSite } from "@/utils/sitePath";
|
||||
import SiteSwitcher from "@/components/SiteSwitcher.vue";
|
||||
import SiteContextCard from "@/components/SiteContextCard.vue";
|
||||
import ThemeSwitcher from "@/components/ThemeSwitcher.vue";
|
||||
import config from "@/assets/config/host.config";
|
||||
|
||||
@@ -42,9 +43,20 @@ const pathNavStyle = computed(() => (isAlone.value ? { left: "0", width: "100%"
|
||||
const school = computed(() => store.currentSchool);
|
||||
const isSuper = computed(() => store.isSuperAdmin);
|
||||
|
||||
/** 当前落点是否院系子站(store 契约):决定身份条徽章与底边色带 */
|
||||
/** 当前落点是否院系子站(store 契约):决定身份条徽章、侧栏/面包屑身份色带与页头站点名(v2 §C.3 #2/#3) */
|
||||
const isSubSite = computed(() => store.isSubSite);
|
||||
const pathNavClass = computed(() => (isSubSite.value ? "band-sub" : "band-main"));
|
||||
/** 身份档位类:同名类挂在 .left-menu 与 .path-nav 两处壳层节点,主站不染色(默认为主站、偏离才提示) */
|
||||
const identityClass = computed(() => (isSubSite.value ? "band-sub" : "band-main"));
|
||||
|
||||
/* 标题带站点(§C.3 #3):页头标题与 document.title 都写成「页面名 · 站点名」,多标签页也能分辨 */
|
||||
const siteName = computed(() => store.currentSite?.name || "");
|
||||
const shellTitle = computed(() => (siteName.value ? `${appName.value} · ${siteName.value}` : appName.value));
|
||||
const docTitle = computed(() => {
|
||||
const t = String((route.meta as any)?.title || "");
|
||||
if (!siteName.value) return t;
|
||||
return t ? `${t} · ${siteName.value}` : siteName.value;
|
||||
});
|
||||
watch(docTitle, v => { if (v) document.title = v; }, { immediate: true });
|
||||
|
||||
/** 菜单/面包屑 page(主站形态完整路径)→ 当前站点形态:子站插入 /site/{code} 段(docs/13 §4)。
|
||||
* 非 /sch/ 前缀(外链模块)原样返回,防拼坏 */
|
||||
@@ -315,7 +327,9 @@ watch(
|
||||
<Icon type="ios-undo" :size="18" />
|
||||
</a>
|
||||
<div v-if="isSuper" class="delimiter fl align-middle"></div>
|
||||
<h4 class="fl align-middle">{{ appName }}</h4>
|
||||
<h4 class="fl align-middle" :title="shellTitle">
|
||||
{{ appName }}<span v-if="siteName" class="shell-site" :class="identityClass"> · {{ siteName }}</span>
|
||||
</h4>
|
||||
<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">
|
||||
@@ -329,8 +343,9 @@ watch(
|
||||
<!-- 配色切换器(fr 顺序:DOM 后置即位于用户胶囊左侧) -->
|
||||
<div class="theme-cap fr"><ThemeSwitcher /></div>
|
||||
</header>
|
||||
<div class="left-menu" v-if="!isAlone">
|
||||
<div class="menu-top-box"></div>
|
||||
<div class="left-menu" :class="identityClass" v-if="!isAlone">
|
||||
<!-- 站点卡(v2 §C.3 #1):占用原本空置的 .menu-top-box,身份常驻侧栏 -->
|
||||
<div class="menu-top-box"><SiteContextCard /></div>
|
||||
<ul>
|
||||
<template v-for="(item, index) in modList" :key="'menu_li_' + index">
|
||||
<template v-if="item.children && item.children.length > 0">
|
||||
@@ -366,7 +381,7 @@ watch(
|
||||
<div class="clear-box"></div>
|
||||
</div>
|
||||
|
||||
<div class="path-nav" :class="pathNavClass" :style="pathNavStyle">
|
||||
<div class="path-nav" :class="identityClass" :style="pathNavStyle">
|
||||
<Breadcrumb separator=">">
|
||||
<!-- 首页段(常驻第一段):旧 BschHome 已下线,统一跳用户中心 userhome -->
|
||||
<a href="javascript:;" @click="linkHomePage">
|
||||
@@ -478,6 +493,42 @@ watch(
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* ===== 站点卡落位与身份染色(v2 §C.3 方案 A · P-C1)=====
|
||||
.menu-top-box 在压缩产物 layout.css 里是「10px 空档 + 右边框」,放卡后必须让位;
|
||||
特异性 .left-menu .menu-top-box[data-v-x](0,3,0)> 全局 .left-menu .menu-top-box(0,2,0),可覆盖。
|
||||
sticky:侧栏是 overflow:auto 的滚动容器,卡片随之滚走就谈不上"身份常驻",故吸附在顶部并垫同色底。 */
|
||||
.left-menu .menu-top-box {
|
||||
height: auto;
|
||||
border-right: none;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
background: var(--side-bg);
|
||||
}
|
||||
/* 子站 3px 身份色条贴在常驻卡区顶部(inset 阴影不占布局、不随内容滚走);主站不画——
|
||||
默认为主站、偏离才提示(§C.3 设计原则),避免两态都染色带来视觉噪音 */
|
||||
.left-menu.band-sub .menu-top-box {
|
||||
box-shadow: inset 0 3px 0 #ff9900;
|
||||
padding-top: 3px;
|
||||
}
|
||||
/* 页头站点名胶囊:站名可能很长,h4 截断(完整「页面名 · 站点名」在 title 与 document.title) */
|
||||
.header-container h4 {
|
||||
max-width: 46vw;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.shell-site {
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
.shell-site.band-sub {
|
||||
background: rgba(255, 153, 0, 0.32);
|
||||
}
|
||||
|
||||
/* 面包屑「首页」垂直居中:layout.css 遗留的 .path-nav .icon-category{position:relative;top:1px}
|
||||
是给老 iconfont 字体字形做的基线补偿,换成 ysch mask 的 1em 方块后反而把图标顶偏;
|
||||
这里用 inline-flex 真居中并关掉该补偿(不动压缩产物 layout.css,覆盖收在本组件内)。
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useCmsStore } from "@/store";
|
||||
import { adminPathOf, schoolSubPath } from "@/utils/sitePath";
|
||||
|
||||
/**
|
||||
* 站点卡(v2 §C.3 方案 A · P-C1):侧栏常驻的「站点身份 + 站点切换」卡,占掉 SchoolLayout 的空 .menu-top-box。
|
||||
* 动因(§C.2):站点身份此前只活在面包屑最后一格的 SiteSwitcher 里,进入工作区后侧栏/页头零标识,
|
||||
* 主站与院系子站看起来像同一个站。方案 A 的口径是身份常驻在管理壳(Chrome Identity),功能菜单仍只有一份。
|
||||
* 数据源 store.schoolSites(= user.sites 按本校过滤,含 SiteVo.theme),零后端改动。
|
||||
* 切换语义与 SiteSwitcher 一致:只改 URL(补/省 /site/{code} 段)并停留当前功能页,上下文由路由同步。
|
||||
*/
|
||||
const store = useCmsStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const school = computed(() => store.currentSchool);
|
||||
const isSub = computed(() => store.isSubSite);
|
||||
const currentSite = computed(() => store.currentSite as any);
|
||||
const currentName = computed(() => currentSite.value?.name || "");
|
||||
/** 层级行:学校 › 学校主站 / 学校 › 院系子站(子站是他校站点时无父名可显,退化为站名) */
|
||||
const tierText = computed(() => {
|
||||
const sch = school.value?.schName || school.value?.mainSiteName || "";
|
||||
const tier = isSub.value ? "院系子站" : "学校主站";
|
||||
return sch ? `${sch} › ${tier}` : tier;
|
||||
});
|
||||
/** 皮肤包名(§C.3 #4:子站可独立选皮肤,这是最直观的"两站不同"信号)。取站点列表原始数据,currentSite 精简态不带 theme */
|
||||
const currentTheme = computed(
|
||||
() => (store.schoolSites as any[])?.find((s: any) => s.id === currentSite.value?.id)?.theme || ""
|
||||
);
|
||||
|
||||
const sites = computed(() => (store.schoolSites || []) as any[]);
|
||||
const isMainSite = (site: any) => site?.id === school.value?.mainSiteId;
|
||||
|
||||
/** 当前功能页子路径:点站点只换上下文、不换页面(与 SiteSwitcher.pick 同语义) */
|
||||
const curSub = computed(() => schoolSubPath(route.path) || "/article");
|
||||
|
||||
function pick(site: any) {
|
||||
if (!site) return;
|
||||
const target = adminPathOf(school.value?.mainSiteCode || "", isMainSite(site) ? null : site.code, curSub.value);
|
||||
if (target === route.path) return;
|
||||
router.push(target);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="currentSite" class="site-context-card" :class="{ sub: isSub }">
|
||||
<div class="scc-head">
|
||||
<span class="scc-dot" aria-hidden="true"></span>
|
||||
<div class="scc-id">
|
||||
<b class="scc-name" :title="currentName">{{ currentName }}</b>
|
||||
<span class="scc-tier">{{ tierText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="scc-theme">皮肤包<b>{{ currentTheme || "未设置" }}</b></p>
|
||||
<p class="scc-cap">切换管理站点({{ sites.length }})</p>
|
||||
<ul class="scc-list">
|
||||
<li
|
||||
v-for="s in sites"
|
||||
:key="s.id"
|
||||
class="scc-site"
|
||||
:class="{ active: s.id === currentSite.id }"
|
||||
:aria-current="s.id === currentSite.id ? 'true' : null"
|
||||
:title="s.name"
|
||||
@click="pick(s)"
|
||||
>
|
||||
<span class="scc-site-bdg" :class="isMainSite(s) ? 'main' : 'sub'">
|
||||
{{ isMainSite(s) ? "主站" : "院系" }}
|
||||
</span>
|
||||
<span class="scc-site-name">{{ s.name }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 配色复用 SiteSwitcher 的定稿身份色(主站 #577afb / 子站 #ff9900 / 子站淡底 #fff7ec),不引新色值。
|
||||
侧栏默认档是深色渐变(yh-theme.css --side-bg),故卡用白底浮起面板 + 8px 右左缩进,
|
||||
与 .left-menu li{margin:1px 8px} 的栅格对齐,深浅两档都成立。 */
|
||||
.site-context-card {
|
||||
box-sizing: border-box;
|
||||
margin: 8px 8px 2px;
|
||||
padding: 10px 10px 8px;
|
||||
border-radius: var(--r-s, 7px);
|
||||
background: #fff;
|
||||
color: #272829;
|
||||
font-size: 12px;
|
||||
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.08);
|
||||
&.sub {
|
||||
background: #fff7ec;
|
||||
.scc-name {
|
||||
color: #d48806;
|
||||
}
|
||||
}
|
||||
}
|
||||
.scc-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
.scc-dot {
|
||||
flex-shrink: 0;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-top: 4px;
|
||||
border-radius: 50%;
|
||||
background: #577afb;
|
||||
}
|
||||
.sub .scc-dot {
|
||||
background: #ff9900;
|
||||
}
|
||||
.scc-id {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.scc-name {
|
||||
font-size: 14px;
|
||||
color: #4465d4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.scc-tier {
|
||||
margin-top: 2px;
|
||||
color: #969799;
|
||||
line-height: 16px;
|
||||
}
|
||||
.scc-theme {
|
||||
margin: 6px 0 0;
|
||||
color: #969799;
|
||||
line-height: 18px;
|
||||
b {
|
||||
color: #656667;
|
||||
font-weight: normal;
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
.scc-cap {
|
||||
margin: 6px 0 2px;
|
||||
padding-top: 6px;
|
||||
border-top: 1px solid #f0f2f5;
|
||||
color: #c0c4cc;
|
||||
line-height: 18px;
|
||||
}
|
||||
.scc-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
/* RK-07 缓解:院系子站多时卡内限高滚动,不把侧栏撑成一屏长的站点列表
|
||||
(>8 站的"最近访问 + 全部"两段式属后续项,未在本工作项清单内) */
|
||||
max-height: 40vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.scc-site {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 4px;
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
&:hover {
|
||||
background: #eff4ff;
|
||||
}
|
||||
&.active {
|
||||
background: #eff4ff;
|
||||
.scc-site-name {
|
||||
color: #4465d4;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
.scc-site-bdg {
|
||||
flex-shrink: 0;
|
||||
font-size: 10px;
|
||||
line-height: 16px;
|
||||
padding: 0 5px;
|
||||
border-radius: 3px;
|
||||
color: #fff;
|
||||
&.main {
|
||||
background: #577afb;
|
||||
}
|
||||
&.sub {
|
||||
background: #ff9900;
|
||||
}
|
||||
}
|
||||
.scc-site-name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user