[CMS-VISUAL] 站点管理改版: 表格→站点工作台(主站宽卡+院系子站卡片网格)+ 页头承载站点身份 + v3.1 视觉修订(182 测试绿)
build / build (push) Successful in 2m20s

- SiteManage.vue: 8 列表格改为站点工作台,按 schId 分组「主站宽卡 + 院系子站网格」,
  保留卡片⇄表格视图切换与搜索过滤;scope=all 全局视图支持多校分组,搜索时主站卡始终保留(层级锚点)
- 新增 views/cms/components/SiteCard.vue、SiteMainCard.vue
- 删除 components/SiteContextCard.vue: 站点身份改由页头「学校名.站点名」承担(SchoolLayout),
  侧栏不再铺第二份站点列表,切站入口从 3 处收敛到 2 处(面包屑 + 站点工作台)
- v3.1 视觉修订: 色带语义统一为「当前站」(全页同时只有一张卡带选中态,不再一页两处蓝);
  删除面包屑 band-sub 橙色接缝条(身份已收回页头);返回按钮 ios-undo 18×18 → ios-arrow-back 26×26 可点方块;
  删除 SiteManage .zone-tip 设计说明文字(误当 UI 渲染)
- 测试: siteLayer.spec.ts 站点卡 4 用例重写为页头反向/正向断言,siteManage.spec.ts 补卡片视图断言;182 用例绿
- 方案: docs/22-站点管理修订方案.md(含 §9 v3.1 视觉修订与像素对照表)
This commit is contained in:
chenxing
2026-09-28 18:32:13 +08:00
parent b6b022e4ca
commit cff4325947
8 changed files with 902 additions and 309 deletions
+58 -32
View File
@@ -15,7 +15,6 @@ 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";
@@ -48,13 +47,27 @@ const isSubSite = computed(() => store.isSubSite);
/** 身份档位类:同名类挂在 .left-menu 与 .path-nav 两处壳层节点,主站不染色(默认为主站、偏离才提示) */
const identityClass = computed(() => (isSubSite.value ? "band-sub" : "band-main"));
/* 标题带站点(§C.3 #3):页头标题与 document.title 都写成「页面名 · 站点名」,多标签页也能分辨 */
/* 页头身份(docs/22 v3):学校名.站点名 —— 一处文字同时回答「哪个学校 + 哪个站点」。
子站取站点名(云海大学.机电学院),主站固定写「主站」(云海大学.主站);
沿用既有 .shell-site 胶囊语汇(band-sub 橙 / band-main 中性白),不引新色值。 */
const siteName = computed(() => store.currentSite?.name || "");
const shellTitle = computed(() => (siteName.value ? `${appName.value} · ${siteName.value}` : appName.value));
const schoolName = computed(() => school.value?.schName || school.value?.mainSiteName || "");
/** 页头站点档位文案:主站不写站全名(会很长),统一「主站」 */
const siteLabel = computed(() => (isSubSite.value ? siteName.value : "主站"));
/** 完整身份串:学校名.站点名(学校未就绪时退化为站点名) */
const identityText = computed(() => {
const label = siteLabel.value;
if (!label) return "";
return schoolName.value ? `${schoolName.value}.${label}` : label;
});
const shellTitle = computed(() =>
identityText.value ? `${appName.value} · ${identityText.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;
// 多标签页可分辨:文档标题同样带「学校名.站点名」,与页头口径一致
if (!identityText.value) return t;
return t ? `${t} · ${identityText.value}` : identityText.value;
});
watch(docTitle, v => { if (v) document.title = v; }, { immediate: true });
@@ -324,11 +337,17 @@ watch(
title="返回学校列表"
@click="goWorkbench"
>
<Icon type="ios-undo" :size="18" />
<Icon type="ios-arrow-back" :size="20" />
</a>
<div v-if="isSuper" class="delimiter fl align-middle"></div>
<h4 class="fl align-middle" :title="shellTitle">
{{ appName }}<span v-if="siteName" class="shell-site" :class="identityClass"> · {{ siteName }}</span>
{{ appName }}<template v-if="identityText"
><span class="h-delimiter"></span
><span class="school-cap" :class="identityClass"
>{{ schoolName }}<span class="sc-sep">.</span
><span class="sc-site">{{ siteLabel }}</span></span
></template
>
</h4>
<div class="header-aside fr">
<img :src="userInfo.avatar" v-if="userInfo.avatar" class="fl align-middle" @click="linkUserInfo" />
@@ -344,8 +363,8 @@ watch(
<div class="theme-cap fr"><ThemeSwitcher /></div>
</header>
<div class="left-menu" :class="identityClass" v-if="!isAlone">
<!-- 站点卡(v2 §C.3 #1):占用原本空置的 .menu-top-box,身份常驻侧栏 -->
<div class="menu-top-box"><SiteContextCard /></div>
<!-- 站点卡已于 docs/22 v3 撤除(与站点工作台重复);身份改由页头「学校名.站点名」承担 -->
<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">
@@ -444,13 +463,23 @@ watch(
</template>
<style scoped lang="scss">
/* 超管返回学校列表图标:header 常驻入口(校级账号不渲染) */
/* 超管返回学校列表:header 常驻入口(校级账号不渲染)。
与「首页」按钮同规格的可点方块(26×26 + hover 底)+ 左右 10px 呼吸 ——
原先是裸 18×18 图标夹在两条分隔线之间(x=126/146),既无点击热区、又比首页按钮小一档;
图标同时从 ios-undo(字形像「拐角」)换成 ios-arrow-back。 */
.back-workbench {
width: 26px;
height: 26px;
margin: 0 10px;
border-radius: 5px;
color: #fff;
display: inline-flex;
align-items: center;
justify-content: center;
transition: background 0.16s;
&:hover {
opacity: 0.85;
background: rgba(255, 255, 255, 0.16);
opacity: 1;
}
}
.no-site-tip {
@@ -493,39 +522,36 @@ 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) */
/* 页头身份(docs/22 v3):云海CMS | 学校名.站点名
h4 截断——学校名+站名可能很长,完整串在 :title 与 document.title。
胶囊色沿用原 .shell-site 取值(主站 rgba(255,255,255,.18) / 子站橙 rgba(255,153,0,.32)),不引新色值。 */
.header-container h4 {
max-width: 46vw;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.shell-site {
.h-delimiter {
display: inline-block;
width: 1px;
height: 14px;
margin: 0 10px;
vertical-align: -2px;
background: rgba(255, 255, 255, 0.4);
}
.school-cap {
font-size: 13px;
font-weight: normal;
}
.sc-sep {
opacity: 0.75;
}
.sc-site {
padding: 2px 6px;
border-radius: 3px;
background: rgba(255, 255, 255, 0.18);
}
.shell-site.band-sub {
.school-cap.band-sub .sc-site {
background: rgba(255, 153, 0, 0.32);
}
-194
View File
@@ -1,194 +0,0 @@
<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>