Files
chenxing f29c193ea7 feat(theme+规范): 管理端接入 yh-theme 主题层并按 bsch 应用页规范升级页面骨架
- 主题层: scripts/gen-yh-theme.mjs 生成 assets/styles/yh-theme.css(构建期静态化 color-mix),
  5 主题档×侧栏深浅×渐变×side-plain 挂 html 档位类名; main.ts 于 icons.css 后引入并 initTheme() 消首帧闪色
- utils/theme.ts(+8 用例): localStorage(yh-theme) 持久化 + <html> 档位类名幂等切换
- ThemeSwitcher.vue(+6 用例): 顶栏配色胶囊(5 色网格+3 开关), 定稿 07-seam-pure 同款皮肤
- SchoolLayout: 顶栏 logo 徽标+配色切换器挂载; 站点身份色带退役, 子站橙由 yh-theme .band-sub::before 融合
- useTableHeight 响应化(+5 用例): ResizeObserver(body) 重算修复窗口缩放冻结, 常量 56/56 档不动
- bsch 规范骨架: 操作区 fix mb-12/次按钮 fr, Table 补 link-table table-bottom-box,
  文章关键字 200px 即输即搜(300ms 防抖去 enter-button), 附件分页 customPageCount 记忆+page-box fr,
  轮播展示位移入搜索区, 友链分组改 tabs-wrapper+Tabs card, 五表单校验失败统一"请填写红色星号内容!"
- 142 用例全绿, vite build 通过
2026-09-19 16:48:16 +08:00

250 lines
14 KiB
JavaScript
Raw Permalink 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.
/**
* yh-theme.css 生成器(《管理端整体主题迁移方案.md》§6.4:color-mix 构建期静态化)。
* 定稿源:07-seam-pure.html(浅色接缝条·纯享版 2026-09-11)。
* 用法:node scripts/gen-yh-theme.mjs → src/assets/styles/yh-theme.css
* 运行时零 color-mix,兼容下限=支持 CSS 变量。
*/
import { writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const hex = n => Math.round(n).toString(16).padStart(2, "0");
const parse = c => [parseInt(c.slice(1, 3), 16), parseInt(c.slice(3, 5), 16), parseInt(c.slice(5, 7), 16)];
/** 与 CSS color-mix(in srgb, A w%, B) 等价的线性混合 */
const mix = (a, b, w) => {
const [r1, g1, b1] = parse(a);
const [r2, g2, b2] = parse(b);
return `#${hex(r1 * w + r2 * (1 - w))}${hex(g1 * w + g2 * (1 - w))}${hex(b1 * w + b2 * (1 - w))}`;
};
const rgba = (c, a) => {
const [r, g, b] = parse(c);
return `rgba(${r},${g},${b},${a})`;
};
// 定稿五档品牌色(07-seam-pure.html L31-35)
const THEMES = {
blue: { brand: "#577afb", b2: "#7b8ffd", dark: "#4264e4", soft: "#eef2ff", a: 0.34, glow: 0.42 },
ocean: { brand: "#2f7ce0", b2: "#4a9bf5", dark: "#2568c4", soft: "#eaf3fe", a: 0.34, glow: 0.42 },
ink: { brand: "#3d4d63", b2: "#6d82a1", dark: "#2e3c4f", soft: "#eef1f6", a: 0.34, glow: 0.5 },
violet: { brand: "#6d5ae0", b2: "#8f7cf2", dark: "#5a48c9", soft: "#f1effe", a: 0.34, glow: 0.42 },
teal: { brand: "#0d9488", b2: "#18b5a5", dark: "#0b7c72", soft: "#e8f7f5", a: 0.36, glow: 0.42 },
};
const NAVY = "#0d1424"; // side-deep 混色基底
const WHITE = "#ffffff";
const sideTokens = t => ({
deepSolid: mix(t.brand, NAVY, 0.72),
deepGradTop: mix(t.brand, WHITE, 0.96),
deepGradBot: mix(t.brand, NAVY, 0.7),
lightSolid: mix(t.brand, WHITE, 0.07),
lightGradTop: mix(t.brand, WHITE, 0.13),
lightGradBot: mix(t.brand, "#f5f7fa", 0.03),
lightLine: mix(t.brand, "#e6eaf0", 0.18),
lightHover: mix(t.brand, "#f2f4f8", 0.1),
lightActive: mix(t.brand, WHITE, 0.15),
strip: mix(t.brand, WHITE, 0.13), // 接缝条渐隐起点
});
const B = THEMES.blue;
const BS = sideTokens(B);
let css = `/* 本文件由 scripts/gen-yh-theme.mjs 生成(color-mix 已构建期静态化),勿手改派生色值。
定稿:07-seam-pure.html(浅色接缝条·纯享版);挂接:main.ts 置于 icons.css 之后。
档位开关挂在 <html>:data-theme=blue|ocean|ink|violet|teal × side-deep|side-light × grad-on|grad-off × side-plain */
/* ============ 变量体系(:root 默认档 = blue + side-deep + grad-on + side-follow) ============ */
:root{
--brand:${B.brand}; --brand-2:${B.b2}; --brand-dark:${B.dark};
--brand-soft:${B.soft}; --brand-a:${rgba(B.brand, B.a)}; --glow:${rgba(B.brand, B.glow)};
--topbar-bg:linear-gradient(100deg,var(--brand) 0%,var(--brand-2) 100%);
--green:#20a162; --green-d:#188451;
--red:#e54d42; --red-soft:#fef1f0;
/* 第一波(P1)沿用现状 56/56,紧凑化 52/44 留 P2(方案 §8-D2) */
--topbar-h:56px; --crumb-h:56px; --side-w:200px; --r:10px; --r-s:7px;
--page-bg:#f3f5fa; --panel:#fff; --line:#e9edf5; --line-2:#f0f3f9;
--text:#1e2635; --text-2:#5a6478; --text-3:#98a1b2;
--sh:0 1px 2px rgba(16,24,40,.04),0 14px 30px -22px rgba(16,24,40,.4);
--side-bg:linear-gradient(180deg,${BS.deepGradTop} 0%,${BS.deepGradBot} 100%);
--side-text:#c7d2ea; --side-icon:#95a6cb;
--side-line:rgba(255,255,255,.08);
--side-hover:rgba(255,255,255,.09); --side-hover-text:#fff;
--side-active-bg:rgba(255,255,255,.18); --side-active-text:#fff;
--side-bar:#fff; --side-scroll:rgba(255,255,255,.18);
--strip-bg:linear-gradient(180deg,${BS.strip} 0,#fff 100%);
}
`;
// 每档主题:品牌 token + 侧栏底色四态 + 接缝条起点色(非底色 token 与主题无关,只写一次)
for (const [name, t] of Object.entries(THEMES)) {
const s = sideTokens(t);
css += `
/* ${name} · brand ${t.brand} */
html[data-theme="${name}"]{
--brand:${t.brand}; --brand-2:${t.b2}; --brand-dark:${t.dark};
--brand-soft:${t.soft}; --brand-a:${rgba(t.brand, t.a)}; --glow:${rgba(t.brand, t.glow)};
--strip-bg:linear-gradient(180deg,${s.strip} 0,#fff 100%);
}
html[data-theme="${name}"].side-deep{--side-bg:${s.deepSolid}}
html[data-theme="${name}"].side-deep.grad-on{--side-bg:linear-gradient(180deg,${s.deepGradTop} 0%,${s.deepGradBot} 100%)}
html[data-theme="${name}"].side-light{--side-bg:${s.lightSolid}}
html[data-theme="${name}"].side-light.grad-on{--side-bg:linear-gradient(180deg,${s.lightGradTop} 0%,${s.lightGradBot} 100%)}
html[data-theme="${name}"].grad-off{--topbar-bg:${t.brand}}
`;
}
css += `
/* ---- 深浅档非底色 token(与品牌色联动的用 var 派生;浅色档 line/hover/active 用默认档实测值,
主题间差异肉眼不可辨,保持单档以压复杂度——同定稿 side-light 组) ---- */
html.side-light{
--side-text:#4a5568; --side-icon:#98a2b3;
--side-line:${BS.lightLine};
--side-hover:${BS.lightHover}; --side-hover-text:#1e2635;
--side-active-bg:${BS.lightActive}; --side-active-text:var(--brand-dark);
--side-bar:var(--brand); --side-scroll:#d3d9e3;
}
html.grad-off{--topbar-bg:var(--brand)}
/* ---- 侧栏与品牌色解耦(side-follow 关) ---- */
html.side-plain.side-deep{
--side-bg:#2c3444; --side-text:#c2cbd9; --side-icon:#909dae;
--side-line:rgba(255,255,255,.08);
--side-hover:rgba(255,255,255,.08); --side-hover-text:#fff;
--side-active-bg:rgba(255,255,255,.16); --side-active-text:#fff;
--side-bar:#fff; --side-scroll:rgba(255,255,255,.18);
}
html.side-plain.side-light{
--side-bg:#f7f8fa; --side-text:#4a5568; --side-icon:#98a2b3;
--side-line:#e6eaf0; --side-hover:#eff1f5;
--side-active-bg:#eceef3; --side-active-text:#2f3a4c;
--side-bar:#8b95a8; --side-scroll:#d3d9e3;
}
html.side-plain.grad-on.side-deep{--side-bg:linear-gradient(180deg,#333c4d 0%,#272e3c 100%)}
html.side-plain.grad-on.side-light{--side-bg:linear-gradient(180deg,#fafbfc 0%,#f2f4f7 100%)}
html.side-plain{--strip-bg:linear-gradient(180deg,#f2f4f8 0,#fff 100%)}
`;
/* ============================ 皮肤层(§6.2 类名映射表,不改现有类名) ============================ */
css += `
/* ---- 顶栏:.header-container ---- */
.layout-wrap .header-container{background:var(--topbar-bg);box-shadow:0 1px 3px rgba(16,24,40,.14);transition:background .28s}
.layout-wrap .header-container h4{font-weight:600;letter-spacing:.4px}
.layout-wrap .header-container .delimiter{background:rgba(255,255,255,.28)}
/* 顶栏 logo 徽标(模板新增节点) */
.logo-badge{width:30px;height:30px;flex:0 0 auto;border-radius:9px;background:#fff;display:grid;place-items:center;box-shadow:0 2px 7px rgba(0,0,0,.16);margin-right:10px}
.logo-badge svg,.logo-badge img{width:22px;height:22px;display:block}
/* 用户胶囊 */
.layout-wrap .header-aside{padding:0 8px 0 5px;height:36px;border-radius:999px;transition:background .16s}
.layout-wrap .header-aside:hover{background:rgba(255,255,255,.16)}
/* ---- 侧栏:.left-menu(皮肤覆盖 layout.css 浅色档;深浅由 --side-* 驱动) ---- */
.layout-wrap .left-menu{background:var(--side-bg);border-right:1px solid var(--side-line);color:var(--side-text);transition:background .28s}
.layout-wrap .left-menu::-webkit-scrollbar{width:8px}
.layout-wrap .left-menu::-webkit-scrollbar-thumb{background:var(--side-scroll);border-radius:8px}
.layout-wrap .left-menu ul{padding:8px 0 18px}
.layout-wrap .left-menu li{margin:1px 8px;border-radius:var(--r-s);transition:background .15s,color .15s}
.layout-wrap .left-menu li a{
display:block;height:40px;line-height:40px;padding:0 12px;border-radius:var(--r-s);
color:var(--side-text);font-size:13px;border-right:none;transition:background .15s,color .15s;position:relative;
}
.layout-wrap .left-menu li a:hover{background:var(--side-hover);color:var(--side-hover-text)}
.layout-wrap .left-menu li a:hover .iconfont,.layout-wrap .left-menu li:hover .iconfont{color:var(--side-hover-text)}
.layout-wrap .left-menu li.active{background:var(--side-active-bg);border-right:0}
.layout-wrap .left-menu li.active a{color:var(--side-active-text);font-weight:600;background:transparent;border-right:none}
.layout-wrap .left-menu li.active a .iconfont{color:var(--side-active-text)}
.layout-wrap .left-menu li.active::before{
content:"";position:absolute;left:0;top:50%;transform:translateY(-50%);
width:3px;height:20px;border-radius:0 3px 3px 0;background:var(--side-bar);
}
.layout-wrap .left-menu li.child-item{margin:0 8px}
.layout-wrap .left-menu li.child-item a{height:36px;line-height:36px;font-size:12.5px}
.layout-wrap .left-menu li.child-item.active::before{height:16px}
/* 菜单图标 mask 类(icons.css 用 background-color 上色,这里给品牌感近色) */
.layout-wrap .left-menu .iconfont{margin-right:8px}
.layout-wrap .left-menu .arrow-up,.layout-wrap .left-menu .arrow-down{border-color:var(--side-icon)}
/* ---- 面包屑:.path-nav → 浅色接缝条(方案 §7.3 融合站点身份带) ---- */
.layout-wrap .path-nav{
height:var(--crumb-h);background:var(--strip-bg);border-bottom:1px solid var(--line);
color:var(--text-3);box-sizing:border-box;
}
.layout-wrap .path-nav .ivu-breadcrumb-item-link{color:var(--text-2)}
.layout-wrap .path-nav .ivu-breadcrumb>span:last-child .ivu-breadcrumb-item-link,.layout-wrap .path-nav .ivu-breadcrumb>span:last-child{color:var(--text);font-weight:600}
.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 由接缝条承接顶栏渐变;子站让位身份橙 */
.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}
/* ---- 内容区:灰底 + 白卡 ---- */
.base-wrap-main{background:var(--page-bg)}
.base-wrap-main .main-container{
background:var(--panel);border:1px solid var(--line);border-radius:var(--r);
box-shadow:var(--sh);margin:0 18px 24px;padding:16px 20px 24px;box-sizing:border-box;
}
/* 栏目工作台左树卡 */
.base-wrap-main .tree-container{background:var(--panel);border:1px solid var(--line);border-radius:var(--r);box-shadow:var(--sh)}
/* ---- 搜索区分区化(卡片内 filterbar 语义) ---- */
.base-wrap-main .search-wrap{
background:#fbfcfe;border:1px solid var(--line-2);border-radius:var(--r-s);
padding:10px 12px;height:auto;min-height:52px;box-sizing:border-box;margin-bottom:12px;
}
/* ---- 表格/操作列皮肤(三蓝归一 #5979EA/#2d8cf0 → var(--brand)) ---- */
.ivu-table-header th,.ivu-table-fixed-header th{background:#f7f9fc !important;color:var(--text-2)}
.ivu-table td{border-bottom:1px solid var(--line-2)}
.ivu-table-body tr:hover td,.ivu-table tbody tr:hover td{background:var(--brand-soft) !important}
.base-wrap-main .ivu-table .operating-link{color:var(--brand)}
.base-wrap-main .ivu-table .operating-link.disabled{color:#aaa}
.ivu-table-cell .operating-btn:hover{background:var(--brand);color:#fff}
/* ---- view-ui-plus 品牌态 ---- */
.ivu-btn-primary{background-color:var(--brand);border-color:var(--brand)}
.ivu-btn-primary:hover,.ivu-btn-primary.active{background-color:var(--brand-dark);border-color:var(--brand-dark)}
.ivu-btn-success{background-color:var(--green);border-color:var(--green)}
.ivu-btn-success:hover,.ivu-btn-success.active{background-color:var(--green-d);border-color:var(--green-d)}
.ivu-btn-error{background-color:var(--red);border-color:var(--red)}
.ivu-btn-error:hover,.ivu-btn-error.active{background-color:#c93f36;border-color:#c93f36}
.ivu-btn-text{color:var(--brand)}
.ivu-btn-text:hover{color:var(--brand-dark)}
.ivu-tabs-ink-bar{background-color:var(--brand)}
.ivu-tabs-tab:hover,.ivu-tabs-tab-active{color:var(--brand)}
.base-wrap-main .ivu-page-item-active{background:var(--brand);border-color:var(--brand)}
.base-wrap-main .ivu-page-item-active a,.base-wrap-main .ivu-page-item-active a:hover{color:#fff !important}
.base-wrap-main .ivu-page-item:hover{border-color:var(--brand)}
.base-wrap-main .ivu-page-item:hover a,.base-wrap-main .ivu-page-item:hover i{color:var(--brand)}
.base-wrap-main .ivu-page-prev:hover i,.base-wrap-main .ivu-page-next:hover i{border-color:var(--brand);color:var(--brand)}
.ivu-select-item-selected{color:var(--brand)}
.ivu-select-item-selected:hover{background:var(--brand-soft)}
.ivu-checkbox-checked .ivu-checkbox-inner{border-color:var(--brand);background-color:var(--brand)}
.ivu-radio-checked .ivu-radio-inner{border-color:var(--brand);background-color:var(--brand)}
.ivu-checkbox-wrapper:hover .ivu-checkbox-inner,.ivu-radio-wrapper:hover .ivu-radio-inner{border-color:var(--brand)}
.ivu-input:focus,.ivu-input-focused,.ivu-select-visible .ivu-select-selection{border-color:var(--brand);box-shadow:0 0 0 2px var(--brand-a)}
.ivu-input:hover,.ivu-select-selection:hover{border-color:#c6d0e2}
.ivu-switch-checked{border-color:var(--brand);background-color:var(--brand)}
.ivu-tree-title:hover{background-color:var(--brand-soft)}
.ivu-tree-title-selected,a.ivu-tree-title-selected:hover{background-color:var(--brand-soft);color:var(--brand)}
.ivu-spin-dot{background-color:var(--brand)}
.ivu-modal-header-inner{font-weight:600;color:var(--text)}
a{color:var(--brand)}
a:hover{color:var(--brand-dark)}
/* ---- 滚动条(定稿全局) ---- */
::-webkit-scrollbar{width:8px;height:8px}
::-webkit-scrollbar-thumb{background:#c9d0dd;border-radius:8px}
::-webkit-scrollbar-thumb:hover{background:#aeb8c9}
::-webkit-scrollbar-track{background:transparent}
/* ---- 工作台/主题管理全屏页深色顶栏吃主题变量(方案 §7.3) ---- */
.wb-header{background:var(--topbar-bg) !important}
`;
writeFileSync(resolve(root, "src/assets/styles/yh-theme.css"), css, "utf8");
console.log("yh-theme.css written:", Buffer.byteLength(css), "bytes");