From f29c193ea791af808b27a18c95842bd5de54d87e Mon Sep 17 00:00:00 2001 From: chenxing Date: Sat, 19 Sep 2026 16:48:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(theme+=E8=A7=84=E8=8C=83):=20=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E7=AB=AF=E6=8E=A5=E5=85=A5=20yh-theme=20=E4=B8=BB?= =?UTF-8?q?=E9=A2=98=E5=B1=82=E5=B9=B6=E6=8C=89=20bsch=20=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E9=A1=B5=E8=A7=84=E8=8C=83=E5=8D=87=E7=BA=A7=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E9=AA=A8=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 主题层: 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) 持久化 + 档位类名幂等切换 - 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 通过 --- scripts/gen-yh-theme.mjs | 249 ++++++++++++++++++ src/assets/images/yunhai-logo.svg | 7 + src/assets/styles/yh-theme.css | 230 ++++++++++++++++ src/components/SchoolLayout.vue | 27 +- src/components/ThemeSwitcher.spec.ts | 115 ++++++++ src/components/ThemeSwitcher.vue | 274 ++++++++++++++++++++ src/components/article/ArticleListPanel.vue | 21 +- src/main.ts | 4 + src/utils/index.ts | 63 +++-- src/utils/theme.spec.ts | 94 +++++++ src/utils/theme.ts | 84 ++++++ src/utils/useTableHeight.spec.ts | 94 +++++++ src/views/cms/ArticleEdit.vue | 5 +- src/views/cms/AttachmentManage.vue | 20 +- src/views/cms/BannerManage.vue | 30 ++- src/views/cms/CategoryManage.vue | 6 +- src/views/cms/LinkManage.vue | 19 +- src/views/cms/SiteManage.vue | 4 +- src/views/cms/SiteSetting.vue | 25 +- 19 files changed, 1299 insertions(+), 72 deletions(-) create mode 100644 scripts/gen-yh-theme.mjs create mode 100644 src/assets/images/yunhai-logo.svg create mode 100644 src/assets/styles/yh-theme.css create mode 100644 src/components/ThemeSwitcher.spec.ts create mode 100644 src/components/ThemeSwitcher.vue create mode 100644 src/utils/theme.spec.ts create mode 100644 src/utils/theme.ts create mode 100644 src/utils/useTableHeight.spec.ts diff --git a/scripts/gen-yh-theme.mjs b/scripts/gen-yh-theme.mjs new file mode 100644 index 0000000..bea1a13 --- /dev/null +++ b/scripts/gen-yh-theme.mjs @@ -0,0 +1,249 @@ +/** + * 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 之后。 + 档位开关挂在 :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"); diff --git a/src/assets/images/yunhai-logo.svg b/src/assets/images/yunhai-logo.svg new file mode 100644 index 0000000..7d1d358 --- /dev/null +++ b/src/assets/images/yunhai-logo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/styles/yh-theme.css b/src/assets/styles/yh-theme.css new file mode 100644 index 0000000..8d53204 --- /dev/null +++ b/src/assets/styles/yh-theme.css @@ -0,0 +1,230 @@ +/* 本文件由 scripts/gen-yh-theme.mjs 生成(color-mix 已构建期静态化),勿手改派生色值。 + 定稿:07-seam-pure.html(浅色接缝条·纯享版);挂接:main.ts 置于 icons.css 之后。 + 档位开关挂在 :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:#577afb; --brand-2:#7b8ffd; --brand-dark:#4264e4; + --brand-soft:#eef2ff; --brand-a:rgba(87,122,251,0.34); --glow:rgba(87,122,251,0.42); + --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,#5e7ffb 0%,#415bbb 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,#e9eefe 0,#fff 100%); +} + +/* blue · brand #577afb */ +html[data-theme="blue"]{ + --brand:#577afb; --brand-2:#7b8ffd; --brand-dark:#4264e4; + --brand-soft:#eef2ff; --brand-a:rgba(87,122,251,0.34); --glow:rgba(87,122,251,0.42); + --strip-bg:linear-gradient(180deg,#e9eefe 0,#fff 100%); +} +html[data-theme="blue"].side-deep{--side-bg:#425dbf} +html[data-theme="blue"].side-deep.grad-on{--side-bg:linear-gradient(180deg,#5e7ffb 0%,#415bbb 100%)} +html[data-theme="blue"].side-light{--side-bg:#f3f6ff} +html[data-theme="blue"].side-light.grad-on{--side-bg:linear-gradient(180deg,#e9eefe 0%,#f0f3fa 100%)} +html[data-theme="blue"].grad-off{--topbar-bg:#577afb} + +/* ocean · brand #2f7ce0 */ +html[data-theme="ocean"]{ + --brand:#2f7ce0; --brand-2:#4a9bf5; --brand-dark:#2568c4; + --brand-soft:#eaf3fe; --brand-a:rgba(47,124,224,0.34); --glow:rgba(47,124,224,0.42); + --strip-bg:linear-gradient(180deg,#e4eefb 0,#fff 100%); +} +html[data-theme="ocean"].side-deep{--side-bg:#255fab} +html[data-theme="ocean"].side-deep.grad-on{--side-bg:linear-gradient(180deg,#3781e1 0%,#255da8 100%)} +html[data-theme="ocean"].side-light{--side-bg:#f0f6fd} +html[data-theme="ocean"].side-light.grad-on{--side-bg:linear-gradient(180deg,#e4eefb 0%,#eff3f9 100%)} +html[data-theme="ocean"].grad-off{--topbar-bg:#2f7ce0} + +/* ink · brand #3d4d63 */ +html[data-theme="ink"]{ + --brand:#3d4d63; --brand-2:#6d82a1; --brand-dark:#2e3c4f; + --brand-soft:#eef1f6; --brand-a:rgba(61,77,99,0.34); --glow:rgba(61,77,99,0.5); + --strip-bg:linear-gradient(180deg,#e6e8eb 0,#fff 100%); +} +html[data-theme="ink"].side-deep{--side-bg:#303d51} +html[data-theme="ink"].side-deep.grad-on{--side-bg:linear-gradient(180deg,#455469 0%,#2f3c50 100%)} +html[data-theme="ink"].side-light{--side-bg:#f1f3f4} +html[data-theme="ink"].side-light.grad-on{--side-bg:linear-gradient(180deg,#e6e8eb 0%,#eff2f5 100%)} +html[data-theme="ink"].grad-off{--topbar-bg:#3d4d63} + +/* violet · brand #6d5ae0 */ +html[data-theme="violet"]{ + --brand:#6d5ae0; --brand-2:#8f7cf2; --brand-dark:#5a48c9; + --brand-soft:#f1effe; --brand-a:rgba(109,90,224,0.34); --glow:rgba(109,90,224,0.42); + --strip-bg:linear-gradient(180deg,#eceafb 0,#fff 100%); +} +html[data-theme="violet"].side-deep{--side-bg:#5246ab} +html[data-theme="violet"].side-deep.grad-on{--side-bg:linear-gradient(180deg,#7361e1 0%,#5045a8 100%)} +html[data-theme="violet"].side-light{--side-bg:#f5f3fd} +html[data-theme="violet"].side-light.grad-on{--side-bg:linear-gradient(180deg,#eceafb 0%,#f1f2f9 100%)} +html[data-theme="violet"].grad-off{--topbar-bg:#6d5ae0} + +/* teal · brand #0d9488 */ +html[data-theme="teal"]{ + --brand:#0d9488; --brand-2:#18b5a5; --brand-dark:#0b7c72; + --brand-soft:#e8f7f5; --brand-a:rgba(13,148,136,0.36); --glow:rgba(13,148,136,0.42); + --strip-bg:linear-gradient(180deg,#e0f1f0 0,#fff 100%); +} +html[data-theme="teal"].side-deep{--side-bg:#0d706c} +html[data-theme="teal"].side-deep.grad-on{--side-bg:linear-gradient(180deg,#17988d 0%,#0d6e6a 100%)} +html[data-theme="teal"].side-light{--side-bg:#eef8f7} +html[data-theme="teal"].side-light.grad-on{--side-bg:linear-gradient(180deg,#e0f1f0 0%,#eef4f7 100%)} +html[data-theme="teal"].grad-off{--topbar-bg:#0d9488} + +/* ---- 深浅档非底色 token(与品牌色联动的用 var 派生;浅色档 line/hover/active 用默认档实测值, + 主题间差异肉眼不可辨,保持单档以压复杂度——同定稿 side-light 组) ---- */ +html.side-light{ + --side-text:#4a5568; --side-icon:#98a2b3; + --side-line:#ccd6f2; + --side-hover:#e3e8f8; --side-hover-text:#1e2635; + --side-active-bg:#e6ebfe; --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%)} + +/* ---- 顶栏:.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} diff --git a/src/components/SchoolLayout.vue b/src/components/SchoolLayout.vue index f097521..5195ae7 100644 --- a/src/components/SchoolLayout.vue +++ b/src/components/SchoolLayout.vue @@ -15,6 +15,7 @@ import { resolveMenuPage } from "@/utils/menuRoute"; import { useCmsStore } from "@/store"; import { adminPathOf, stripSiteSeg, schoolSubPath, resolveRouteSite } from "@/utils/sitePath"; import SiteSwitcher from "@/components/SiteSwitcher.vue"; +import ThemeSwitcher from "@/components/ThemeSwitcher.vue"; import config from "@/assets/config/host.config"; const route = useRoute(); @@ -295,6 +296,8 @@ watch(