Files
YschcmsFront/src/components/SiteContextCard.vue
T
chenxing 5d048abdae 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
2026-09-22 21:54:58 +08:00

195 lines
6.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>