+ {{ g.schName || g.main?.name }} +
+ +院系子站
+ {{ g.subs.length }} 个 +没有匹配「{{ keyword }}」的院系子站。
+本校暂无院系子站。
+ +没有匹配「{{ keyword }}」的站点。
+暂无站点,请联系平台管理员在学校工作台添加。
+diff --git a/src/__tests__/siteLayer.spec.ts b/src/__tests__/siteLayer.spec.ts
index dafcfa9..252af1c 100644
--- a/src/__tests__/siteLayer.spec.ts
+++ b/src/__tests__/siteLayer.spec.ts
@@ -298,78 +298,56 @@ describe("docs/13 身份条次级入口与幂等切换(docs/15 T5)", () => {
});
});
-/* ===== v2 §C.3 方案 A(P-C1 / T-C-01~03):站点身份常驻侧栏站点卡 + 壳层染色 + 标题带站点 =====
- 口径:站点卡承载切换与身份,功能菜单仍只有一份(stripSiteSeg 剥段高亮是方案 A 的前置设计,不改)。 */
-describe("P-C1 站点卡:身份常驻侧栏(v2 §C.3 方案 A)", () => {
- it("T-C-01 主站态渲染站点卡:站名 + 层级(学校 › 主站)+ 皮肤包名 + 展开的站点列表(当前站高亮)", async () => {
+/* ===== docs/22 v3:页头身份「学校名.站点名」 + 侧栏站点卡撤除 =====
+ 口径:页头 .school-cap(> .sc-sep + .sc-site)是唯一常驻身份点,主站不写站全名统一「主站」;
+ 切站入口收敛到面包屑站点身份条(T-B-* / SiteSwitcher 用例)与站点工作台卡片两处。
+ 原 v2 的卡内切站用例(T-C-01b)随卡撤除而删除。 */
+describe("P-C1 页头身份与侧栏卡撤除(docs/22 v3)", () => {
+ it("T-C-01 主站态:侧栏 .menu-top-box 复归空档(卡已撤),页头 = 学校名.主站", async () => {
setRoute("/sch/demo/article", { schCode: "demo" });
const el = mountLayout();
await flush();
- // 站点卡挂在侧栏原空位 .menu-top-box 内(零布局改造落点,v2 §C.2)
- const card = el.querySelector(".left-menu .menu-top-box .site-context-card");
- expect(card).toBeTruthy();
- expect(card!.querySelector(".scc-name")!.textContent).toContain("云海大学门户");
- expect(card!.querySelector(".scc-tier")!.textContent).toContain("云海大学");
- expect(card!.querySelector(".scc-tier")!.textContent).toContain("学校主站");
- // 皮肤可见(§C.3 #4):数据源 user.sites[].theme(SiteVo 已带,零后端改动)
- expect(card!.querySelector(".scc-theme")!.textContent).toContain("scholar-blue");
- // 站点列表展开常驻(非下拉):主站 1 + 院系 2 = 3 行,当前站唯一高亮
- const rows = el.querySelectorAll(".scc-site");
- expect(rows).toHaveLength(3);
- expect(el.querySelectorAll(".scc-site.active")).toHaveLength(1);
- expect(rows[0].classList.contains("active")).toBe(true);
- expect(rows[1].textContent).toContain("机电学院");
+ // 站点卡撤除:侧栏顶部空档恢复为空 div(回到加卡之前的 layout.css 形态)
+ const topBox = el.querySelector(".left-menu .menu-top-box");
+ expect(topBox).toBeTruthy();
+ expect(topBox!.children).toHaveLength(0);
+
+ // 页头身份:学校名.站点名 —— 主站档位统一「主站」,不写站全名
+ expect(el.querySelector(".header-container h4")!.textContent).toContain("云海大学.主站");
+ expect(el.querySelector(".header-container .school-cap")!.textContent).toContain("云海大学");
+ expect(el.querySelector(".header-container .sc-site")!.textContent.trim()).toBe("主站");
});
- it("T-C-01b 站点卡点行即切换:只改 URL、停留当前功能页(复用 SiteSwitcher 的 adminPathOf 补段语义)", async () => {
- setRoute("/sch/demo/article", { schCode: "demo" });
- const el = mountLayout();
- await flush();
-
- const jdxy = [...el.querySelectorAll(".scc-site")].find(r => r.textContent?.includes("机电学院"))!;
- jdxy.dispatchEvent(new MouseEvent("click", { bubbles: true }));
- await flush();
- expect(box.push).toHaveBeenCalledWith("/sch/demo/site/jdxy/article");
-
- // 点当前站行幂等:目标 === 当前路径,不产生 push
- box.push.mockClear();
- const main = [...el.querySelectorAll(".scc-site")].find(r => r.textContent?.includes("云海大学门户"))!;
- main.dispatchEvent(new MouseEvent("click", { bubbles: true }));
- await flush();
- expect(box.push).not.toHaveBeenCalled();
- });
-
- it("T-C-02 身份染色:子站态壳层挂 band-sub(卡片 sub 态),主站态挂 band-main 且不染色", async () => {
+ it("T-C-02 身份染色:子站态 .school-cap 挂 band-sub、.sc-site=站名;主站态 band-main、.sc-site=主站", async () => {
setRoute("/sch/demo/site/jdxy/article", { schCode: "demo", siteCode: "jdxy" });
const sub = mountLayout();
await flush();
+ // 侧栏/面包屑色带档位保留(identityClass 仍在 .left-menu / .path-nav 上工作)
expect(sub.querySelector(".left-menu")!.className).toContain("band-sub");
- expect(sub.querySelector(".site-context-card")!.classList.contains("sub")).toBe(true);
- // 皮肤随站而变(§C.3 #4):子站独立皮肤 = 最直观的"两站不同"信号
- expect(sub.querySelector(".scc-theme")!.textContent).toContain("campus-glow");
- // 皮肤缺省也要有身份:色点常驻
- expect(sub.querySelector(".scc-dot")).toBeTruthy();
+ expect(sub.querySelector(".header-container .school-cap")!.classList.contains("band-sub")).toBe(true);
+ expect(sub.querySelector(".header-container .sc-site")!.textContent.trim()).toBe("机电学院");
setRoute("/sch/demo/article", { schCode: "demo" });
const main = mountLayout();
await flush();
expect(main.querySelector(".left-menu")!.className).toContain("band-main");
- expect(main.querySelector(".site-context-card")!.classList.contains("sub")).toBe(false);
+ expect(main.querySelector(".header-container .school-cap")!.classList.contains("band-sub")).toBe(false);
+ expect(main.querySelector(".header-container .sc-site")!.textContent.trim()).toBe("主站");
});
- it("T-C-03 标题带站点:页头标题与 document.title 均为「页面名 · 站点名」,切站后跟随", async () => {
+ it("T-C-03 标题带站点:页头与 document.title 均为「页面名 · 学校名.站点名」,切站后跟随", async () => {
setRoute("/sch/demo/site/jdxy/article", { schCode: "demo", siteCode: "jdxy" });
const el = mountLayout();
await flush();
- expect(document.title).toBe("文章列表 · 机电学院");
- expect(el.querySelector(".header-container h4")!.textContent).toContain("· 机电学院");
+ expect(document.title).toBe("文章列表 · 云海大学.机电学院");
+ expect(el.querySelector(".header-container h4")!.textContent).toContain("云海大学.机电学院");
- // 同一壳内切到主站形态:标题与 document.title 同步换站名
+ // 同一壳内切回主站形态:标题与 document.title 同步为「学校名.主站」
box.route.path = "/sch/demo/article";
box.route.fullPath = "/sch/demo/article";
box.route.params = { schCode: "demo" };
await flush();
- expect(document.title).toBe("文章列表 · 云海大学门户");
+ expect(document.title).toBe("文章列表 · 云海大学.主站");
});
});
diff --git a/src/__tests__/siteManage.spec.ts b/src/__tests__/siteManage.spec.ts
index 0566f22..7c3d7d0 100644
--- a/src/__tests__/siteManage.spec.ts
+++ b/src/__tests__/siteManage.spec.ts
@@ -1,7 +1,7 @@
// @vitest-environment jsdom
/**
- * 站点管理列表页回归(docs/16 主线A 整页化后):
- * 行「编辑」→ 跳站点设置中心 site/setting/:id(弹窗已退役);「新建站点」→ site/new 创建模式;
+ * 站点管理页回归:docs/22 站点工作台(默认卡片视图)+ 表格视图保留。
+ * 行「站点设置」→ 跳站点设置中心 site/setting/:id(弹窗已退役);「新建站点」→ site/new 创建模式;
* 子站行带 URL 站点段(docs/13 adminPathOf 口径)。沿用手写 createApp 挂载桩风格。
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -93,6 +93,13 @@ function clickRowAction(el: ParentNode, iconClass: string, rowIndex = 0) {
return btn;
}
+/** docs/22 后默认视图为卡片;表格行操作用例先切到表格视图 */
+async function switchToTable(el: ParentNode) {
+ const btn = [...el.querySelectorAll(".seg button")].find(b => b.textContent?.includes("表格"));
+ btn!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+ await flush();
+}
+
beforeEach(() => {
vi.clearAllMocks();
siteListMock.mockResolvedValue(JSON.parse(JSON.stringify([siteRow])));
@@ -105,9 +112,10 @@ afterEach(() => {
});
describe("SiteManage 列表页(docs/16 整页化后)", () => {
- it("① 行「编辑」跳站点设置中心(主站形态无站点段)", async () => {
+ it("① 行「站点设置」跳站点设置中心(主站形态无站点段)", async () => {
const el = mountPage();
await flush();
+ await switchToTable(el);
const btn = clickRowAction(el, "icon-edit");
expect(btn).toBeTruthy();
@@ -116,13 +124,14 @@ describe("SiteManage 列表页(docs/16 整页化后)", () => {
expect(routerPushMock).toHaveBeenCalledWith("/sch/demo/site/setting/1");
});
- it("② 子站行「编辑」带 URL 站点段(/sch/{main}/site/{sub}/site/setting/{id})", async () => {
+ it("② 子站行「站点设置」带 URL 站点段(/sch/{main}/site/{sub}/site/setting/{id})", async () => {
siteListMock.mockResolvedValue(JSON.parse(JSON.stringify([
siteRow,
{ ...siteRow, id: 2, code: "jdxy", path: "demo/jdxy", parentSiteId: 1, parentName: "云海大学门户网站", name: "机械学院" },
])));
const el = mountPage();
await flush();
+ await switchToTable(el);
clickRowAction(el, "icon-edit", 1);
await flush();
@@ -143,3 +152,58 @@ describe("SiteManage 列表页(docs/16 整页化后)", () => {
expect(routerPushMock).toHaveBeenCalledWith("/sch/demo/site/new");
});
});
+
+describe("站点工作台(docs/22 v3)", () => {
+ /** 1 主站 + 2 子站:卡片工作台的最小分组样本 */
+ function mockOneMainTwoSubs() {
+ siteListMock.mockResolvedValue(
+ JSON.parse(
+ JSON.stringify([
+ siteRow,
+ { ...siteRow, id: 2, code: "jdxy", path: "demo/jdxy", parentSiteId: 1, parentName: "云海大学门户网站", name: "机电学院" },
+ { ...siteRow, id: 3, code: "wtxy", path: "demo/wtxy", parentSiteId: 1, parentName: "云海大学门户网站", name: "文学院" },
+ ])
+ )
+ );
+ }
+
+ it("T-SM-01 默认卡片视图:主站宽卡 1 + 子站网格 2,当前站唯一高亮;表格视图可切回", async () => {
+ mockOneMainTwoSubs();
+ const el = mountPage(store => {
+ store.user = { superAdmin: true } as any;
+ store.currentSite = { id: 2, code: "jdxy", name: "机电学院" } as any;
+ });
+ await flush();
+
+ expect(el.querySelectorAll(".main-card")).toHaveLength(1);
+ expect(el.querySelectorAll(".site-card")).toHaveLength(2);
+ expect(el.querySelectorAll(".site-card.is-cur")).toHaveLength(1);
+ // 主站卡「子站」统计 = 过滤前真实规模(SiteVo 无栏目/文章字段,仅子站数必显)
+ expect(el.querySelector(".main-card .mc-stats")!.textContent).toContain("2");
+
+ // 切表格视图:卡片网格消失、ivu-table 出现
+ await switchToTable(el);
+ expect(el.querySelector(".card-grid")).toBeNull();
+ expect(el.querySelector(".ivu-table")).toBeTruthy();
+ });
+
+ it("T-SM-02 搜索过滤:命中子站保留、主站宽卡不消失(层级锚点),子站统计不随过滤变化", async () => {
+ mockOneMainTwoSubs();
+ const el = mountPage(store => {
+ store.user = { superAdmin: true } as any;
+ store.currentSite = { id: 1, code: "demo", name: "云海大学门户网站" } as any;
+ });
+ await flush();
+
+ const input = el.querySelector(".seg-search input") as HTMLInputElement;
+ input.value = "机电";
+ input.dispatchEvent(new Event("input", { bubbles: true }));
+ await flush();
+
+ expect(el.querySelectorAll(".main-card")).toHaveLength(1);
+ expect(el.querySelectorAll(".site-card")).toHaveLength(1);
+ expect(el.querySelector(".site-card")!.textContent).toContain("机电学院");
+ // rawSubs 按过滤前全量行统计:仍是 2 个子站
+ expect(el.querySelector(".main-card .mc-stats")!.textContent).toContain("2");
+ });
+});
diff --git a/src/assets/styles/yh-theme.css b/src/assets/styles/yh-theme.css
index 8d53204..adea84d 100644
--- a/src/assets/styles/yh-theme.css
+++ b/src/assets/styles/yh-theme.css
@@ -159,11 +159,12 @@ html.side-plain{--strip-bg:linear-gradient(180deg,#f2f4f8 0,#fff 100%)}
.layout-wrap .path-nav .ivu-breadcrumb a:hover{color:var(--brand)}
.layout-wrap .path-nav .crumb-home{color:var(--brand)}
.layout-wrap .path-nav .ivu-breadcrumb-item-separator{color:#ccd4e0}
-/* 定稿:不再画独立 3px 横杠——顶部 3px 由接缝条承接顶栏渐变;子站让位身份橙 */
+/* 定稿:不再画独立 3px 横杠——顶部 3px 由接缝条承接顶栏渐变。
+ v3:站点身份已由页头「学校名.站点名」承担,原先 band-sub 让位身份橙的覆盖已删,
+ 否则子站态上面包屑顶部常驻一条橙色横杠,与页头身份重复,且被误读成边框。 */
.layout-wrap .path-nav::before{
content:"";position:absolute;left:0;right:0;top:0;height:3px;background:var(--topbar-bg);z-index:1;
}
-.layout-wrap .path-nav.band-sub::before{background:#ff9900}
.layout-wrap .path-nav::after{display:none}
/* ---- 内容区:灰底 + 白卡 ---- */
diff --git a/src/components/SchoolLayout.vue b/src/components/SchoolLayout.vue
index 9715937..49ae33a 100644
--- a/src/components/SchoolLayout.vue
+++ b/src/components/SchoolLayout.vue
@@ -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"
>
-