- 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 新导出)
143 lines
5.3 KiB
Vue
143 lines
5.3 KiB
Vue
<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>
|