feat(theme): 设置页「主题」导航组 + iframe 预览页 + 学校默认档对接(主题设置 S2)

- Settings.vue:新增「主题」导航组(组级不设 adminOnly),mytheme「我的主题」所有人可见、schooltheme「本校默认」item 级 adminOnly + 入 ADMIN_PANES 双保险(NavItem 扩展 adminOnly 字段,navGroups 组级+item 级双层过滤);两个 pane 左色块右预览 iframe——候选档经 ?yh-theme=/yh-side=/yh-grad= URL 覆盖参数拼进 #/theme-preview src,切候选即换 src 重载;档位清单本轮硬编码 5 档(主色取通道 tokens);「跟随学校默认」=删用户键、「跟随系统默认」=clearSchTheme 清空学校配置;当前生效档与来源(我的选择/学校默认/系统默认)经 resolveEffectiveThemeState 标注
- views/ThemePreview.vue(新增):纯展示预览页——顶栏 .header-container + 左菜单 .left-menu + 面包屑 + iview Table 写死 4 行,DOM 结构对齐 Notice layout,观感由通道 CSS 按档位类驱动;根容器 pointer-events:none,无搜索/分页/ThemeSwitcher;挂载时动态 append 通道 init(baseHost + /static/js/yh-theme-init.js,判重防双挂)——仅该 document 吃通道 CSS,UserHome 本体 index.html 不加 loader(定案)
- App.vue:/theme-preview 顶层独立分支(不进桌面框架、不依赖登录态与 isRouterAlive——未登录直开不白屏);菜单白名单加 /theme-preview;登录成功后异步拉 schTheme 写 localStorage[yh-theme-sch-default](失败/空不写不报错,推荐值语义)
- router/routes.ts:登记 /theme-preview(实际渲染走 App.vue 分支;若后续引入守卫须列入免登录白名单——注释已写明)
- BaseApi:getSchTheme / saveSchThemeState / clearSchTheme 三封装
- src/utils/theme.ts:YschTheme npm run sync 下发(版本 20260930-1248-4e12e6d,含 SCHOOL_DEFAULT_STORAGE_KEY 与 resolveEffectiveThemeState 新导出)
This commit is contained in:
chenxing
2026-09-30 12:59:35 +08:00
parent 0ded3cce9f
commit 357c0edb52
6 changed files with 657 additions and 6 deletions
+142
View File
@@ -0,0 +1,142 @@
<template>
<!-- 主题预览页(#/theme-preview,YschTheme《主题设置与通道改动-执行方案》§3.4):
只在本路由的 document 内加载通道(组件挂载时动态 append init 脚本),
档位类与通道 CSS 只落在本 document 的 <html> 上——UserHome 本体(父页)永远不吃通道 CSS。
纯展示:根容器 pointer-events:none,不渲染搜索区/分页操作/ThemeSwitcher。
DOM 结构对齐 Notice layout(header-container / left-menu / path-nav / main-container),
观感(配色/侧栏形态)由通道 base.layout.css 按 URL 覆盖参数(?yh-theme= 等)驱动的档位类提供。 -->
<div class="theme-preview-root">
<header class="header-container fix">
<span class="logo-badge fl"><span class="logo-mark">云海</span></span>
<div class="delimiter fl align-middle"></div>
<h4 class="fl align-middle">管理端预览</h4>
<div class="school-bg"></div>
<div class="header-aside fr">
<span class="name fl align-middle text-ellipsis">演示用户</span>
<div class="delimiter fl align-middle"></div>
<a href="javascript:;" class="logout fl align-middle" title="退出登陆"><i>⎋</i></a>
</div>
</header>
<div class="left-menu">
<div class="menu-top-box"></div>
<ul>
<li v-for="(m, i) in menus" :key="m" :class="{ active: i === 1 }">
<a href="javascript:void(0);"><span class="menu-dot"></span>{{ m }}</a>
</li>
</ul>
<div class="clear-box"></div>
</div>
<div class="path-nav">
<span>首页</span><span class="crumb-sep">/</span><span>通知管理</span><span class="crumb-sep">/</span><span>通知列表</span>
</div>
<div class="preview-main">
<div class="main-container">
<h3 class="panel-title">通知列表</h3>
<Table :columns="columns" :data="rows" size="small" :border="true" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted } from "vue";
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
/**
* 主题预览路由页:档位来源 = URL 覆盖参数(?yh-theme= / ?yh-side= / ?yh-grad=,由设置页 iframe src 拼入,
* 切候选档即换 src 重载),init 启动段按合成优先级挂档——本组件不读写 localStorage、不提供任何交互。
* 直开(不经设置页、未登录)也可渲染:本页由 App.vue 顶层条件分支挂载,不依赖桌面框架与登录态。
*/
const { $conf } = useCurrentInstance();
const menus = ["工作台", "通知管理", "统计分析", "应用中心", "系统设置"];
const columns = [
{ type: "index", title: "序号", width: 70, align: "center" },
{ title: "标题", key: "title" },
{ title: "发布人", key: "author", width: 120, align: "center" },
{ title: "日期", key: "date", width: 160, align: "center" },
];
const rows = [
{ title: "关于国庆节放假安排的通知", author: "校务办", date: "2026-09-28" },
{ title: "期中教学质量检查实施方案", author: "教务处", date: "2026-09-25" },
{ title: "校园安全专项检查通报", author: "保卫处", date: "2026-09-21" },
{ title: "教师信息化能力培训报名", author: "信息中心", date: "2026-09-18" },
];
onMounted(() => {
// 动态引通道 init(仅本 document;UserHome 本体 index.html 不加 loader 是定案)。
// baseHost 即通道域(与 UserBase/Notice 的 loader 同源策略);判重防重复挂载。
if (document.querySelector("script[data-yh-theme-init]")) return;
const s = document.createElement("script");
s.src = `${String($conf.baseHost || "").replace(/\/+$/, "")}/static/js/yh-theme-init.js`;
s.setAttribute("data-yh-theme-init", "1");
document.head.appendChild(s);
});
</script>
<style scoped lang="scss">
/* 本页只补最小布局;配色/圆角/阴影等观感全部由通道 base.layout.css 的档位类驱动 */
.theme-preview-root {
position: fixed;
inset: 0;
overflow: hidden;
pointer-events: none; /* 纯展示:整页不可交互 */
background: #f5f6f8;
font-size: 14px;
}
.header-container {
.logo-mark {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.22);
color: #fff;
font-size: 12px;
font-weight: 600;
}
h4 {
color: #fff;
font-size: 16px;
font-weight: 600;
margin: 0 12px;
}
}
.menu-dot {
display: inline-block;
width: 7px;
height: 7px;
border-radius: 50%;
background: currentColor;
opacity: 0.45;
margin-right: 9px;
vertical-align: middle;
}
.path-nav .crumb-sep {
margin: 0 8px;
color: #c5c8cd;
}
.preview-main {
position: fixed;
top: calc(var(--topbar-h, 52px) + var(--crumb-h, 44px));
left: var(--side-w, 200px);
right: 0;
bottom: 0;
padding: 14px 24px 24px;
overflow: auto;
.panel-title {
font-size: 15px;
font-weight: 600;
color: #2c3138;
margin: 4px 0 12px;
}
}
</style>