From 677c0c47ace0c4210f008e7435632ce2d3262372 Mon Sep 17 00:00:00 2001 From: MeowOS Date: Tue, 1 Sep 2026 16:26:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(meowpanel):=20=E9=9D=A2=E6=9D=BF=20MD3/MIU?= =?UTF-8?q?Ix=20=E5=8F=8C=E4=B8=BB=E9=A2=98=20=C3=97=20=E6=98=8E=E6=9A=97?= =?UTF-8?q?=E5=9B=9B=E5=A5=97=E5=8F=98=E9=87=8F=E8=A1=A8=20+=20=E4=B8=BB?= =?UTF-8?q?=E9=A2=98=E5=88=87=E6=8D=A2=E5=99=A8=EF=BC=88=E8=B7=9F=E9=9A=8F?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F/=E6=B5=8F=E8=A7=88=E5=99=A8/=E6=98=8E/?= =?UTF-8?q?=E6=9A=97=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CSS:色值全量抽为 :root 变量,[data-style]×[data-mode] 四套主题表; MIUIx 色值对齐设备 meow_ui_theme.c(light F7F3EE/F57200、dark 181310/FF8C4A、radius 20/28、spacing 12、细描边); 根属性 data-theme 更名 data-mode - JS:默认联动设备 settings.conf(GET /api/theme 的 ui.style/display.theme); 面板覆盖存 localStorage(meowpanel.style / meowpanel.mode),不写设备; 模式 system/browser/light/dark,browser 走 prefers-color-scheme 并监听日夜切换实时生效; 旧键 meowpanel.theme 自动迁移;顶栏切换器保留原行为(本地生效 + PUT /api/theme 同步设备) - 设置页新增「主题(面板外观)」卡:风格 MD3/MIUIx(可一键回跟随设备)+ 明暗四选一 - 后端未改:/api/theme 已透传 ui.style/display.theme,无需新增 --- patch_v2/meowpanel/web/index.html | 6 +- patch_v2/meowpanel/web/meowpanel.css | 73 +++++++++++++------ patch_v2/meowpanel/web/meowpanel.js | 104 ++++++++++++++++++++++----- 3 files changed, 144 insertions(+), 39 deletions(-) diff --git a/patch_v2/meowpanel/web/index.html b/patch_v2/meowpanel/web/index.html index ba903df..5ed49bc 100644 --- a/patch_v2/meowpanel/web/index.html +++ b/patch_v2/meowpanel/web/index.html @@ -1,5 +1,5 @@ - + @@ -88,8 +88,8 @@
- - + +
diff --git a/patch_v2/meowpanel/web/meowpanel.css b/patch_v2/meowpanel/web/meowpanel.css index f62179e..0a9d2e5 100644 --- a/patch_v2/meowpanel/web/meowpanel.css +++ b/patch_v2/meowpanel/web/meowpanel.css @@ -1,12 +1,12 @@ /* MeowPanel — 前端样式 - * 主题模型:风格族(ui.style: md3 / miuix)× 明暗(display.theme: dark / light) - * 双层变量: - * [data-style] 定形状(radius / spacing / outline 宽)+ 主色(--primary)——对齐 SDK §8.1.3 - * [data-theme] 定中性色 / 语义色(bg/surface/on-surface/outline/error/...) - * 默认(:root 无覆盖)= MD3 dark;MD3 中立形状再显式声明便于对照。 + * 主题模型:风格族(ui.style: md3 / miuix)× 明暗(data-mode: dark / light),四套主题表。 + * [data-style] 定形状(radius / spacing / outline 宽)——对齐 SDK meow_ui_theme.c §8.1.3 + * [data-style][data-mode] 定完整色板(bg/surface/on-surface/outline/primary/error/...), + * MIUIx 色值取自设备端 meow_ui_theme.c 的 MIUIx 配色(暖色基面 + 橙强调)。 + * 默认(index.html 根属性)= MD3 dark;面板内可覆盖(meowpanel.js 写 data-style/data-mode)。 */ -/* ---------- 结构基线 ---------- */ +/* ---------- 结构基线(MD3 形状) ---------- */ :root{ --space:8px; --radius-s:8px; /* 小:chip/内嵌 */ @@ -20,17 +20,17 @@ --speed:.16s; } -/* 风格族:MIUIx 更大圆角 / 略大间距 / 细描边 / 偏品牌主色(更饱和) */ +/* 风格族:MIUIx 更大圆角(20/28,对齐设备 radius/radius_card)/ 更松间距 / 细描边 */ :root[data-style="miuix"]{ - --space:10px; - --radius-s:12px; - --radius-m:18px; /* 按钮 ≈18px */ - --radius-l:24px; /* 卡片 ≈24px */ + --space:12px; + --radius-s:14px; + --radius-m:20px; /* 按钮 ≈20px */ + --radius-l:28px; /* 卡片 ≈28px */ --outline-w:1px; /* 细描边 */ } -/* ---------- 深色(默认主深色) ---------- */ -:root[data-theme="dark"]{ +/* ---------- MD3 × 深色 ---------- */ +:root[data-style="md3"][data-mode="dark"]{ --bg:#0a0a0a; /* 背景底(黑)*/ --surface:#141414; --surface-variant:#1e1e1e; @@ -39,18 +39,16 @@ --on-surface-variant:#a3a3a3; --outline:#3a3a3a; --outline-variant:#2a2a2a; + --primary:#4a8bf5; --on-primary:#ffffff; --error:#cf6679; --on-error:#0a0a0a; --success:#4caf50; --warning:#ffb300; --shadow:0 2px 10px rgba(0,0,0,.5); --scrim:rgba(0,0,0,.55); --hover:rgba(255,255,255,.06); } -/* 深色 × MIUIx 主色更饱和 */ -:root[data-theme="dark"][data-style="md3"]{ --primary:#4a8bf5; --on-primary:#fff; } -:root[data-theme="dark"][data-style="miuix"]{--primary:#ff7a00; --on-primary:#fff; } -/* ---------- 浅色 ---------- */ -:root[data-theme="light"]{ +/* ---------- MD3 × 浅色 ---------- */ +:root[data-style="md3"][data-mode="light"]{ --bg:#f4f5f7; --surface:#ffffff; --surface-variant:#ececee; @@ -59,14 +57,49 @@ --on-surface-variant:#5c5c60; --outline:#d2d2d6; --outline-variant:#e4e4e8; + --primary:#2f6fe0; --on-primary:#ffffff; --error:#ba1a1a; --on-error:#ffffff; --success:#2e7d32; --warning:#ed6c02; --shadow:0 2px 10px rgba(0,0,0,.12); --scrim:rgba(0,0,0,.35); --hover:rgba(0,0,0,.05); } -:root[data-theme="light"][data-style="md3"]{ --primary:#2f6fe0; --on-primary:#fff; } -:root[data-theme="light"][data-style="miuix"]{--primary:#f57200; --on-primary:#fff; } + +/* ---------- MIUIx × 深色(暖黑 181310 + 橙 FF8C4A,色值对齐 meow_ui_theme.c) ---------- */ +:root[data-style="miuix"][data-mode="dark"]{ + --bg:#181310; + --surface:#262019; /* surfaceContainer */ + --surface-variant:#332b25; + --surface-dim:#120e0b; + --on-surface:#ede5dc; + --on-surface-variant:#ab9884; + --outline:#4a3f35; + --outline-variant:#3a3229; + --primary:#ff8c4a; --on-primary:#241100; + --error:#ffb4ab; --on-error:#690005; + --success:#4caf50; --warning:#ffb300; + --shadow:0 2px 10px rgba(0,0,0,.55); + --scrim:rgba(0,0,0,.55); + --hover:rgba(255,255,255,.06); +} + +/* ---------- MIUIx × 浅色(暖白 F7F3EE + 橙 F57200,色值对齐 meow_ui_theme.c) ---------- */ +:root[data-style="miuix"][data-mode="light"]{ + --bg:#f7f3ee; + --surface:#fffbf6; /* surfaceBright */ + --surface-variant:#ede2d7; + --surface-dim:#dfd3c4; + --on-surface:#251a10; + --on-surface-variant:#7d6a5a; + --outline:#cbbfb2; + --outline-variant:#e9dfd0; + --primary:#f57200; --on-primary:#ffffff; + --error:#b3261e; --on-error:#ffffff; + --success:#2e7d32; --warning:#ed6c02; + --shadow:0 2px 10px rgba(60,40,20,.12); + --scrim:rgba(30,20,10,.35); + --hover:rgba(0,0,0,.05); +} /* ---------- 布局 ---------- */ *{ box-sizing:border-box; } diff --git a/patch_v2/meowpanel/web/meowpanel.js b/patch_v2/meowpanel/web/meowpanel.js index 9dca166..b9fc245 100644 --- a/patch_v2/meowpanel/web/meowpanel.js +++ b/patch_v2/meowpanel/web/meowpanel.js @@ -37,7 +37,8 @@ /* ---------------- 全局状态 ---------------- */ const state = { - style: 'md3', theme: 'dark', + style: 'md3', mode: 'dark', // 已解析的风格/明暗(应用到 data-style/data-mode) + devStyle: null, devTheme: null, // 设备 settings.conf 的 ui.style / display.theme(默认来源) conn: 'unknown', filesRel: '/', filesAbs: null, perfTimer: null, // 性能页轮询 @@ -187,39 +188,88 @@ } function bindPwDlg() { /* #content 点击委托在 bindSettings 里处理 pass 按钮;此处无需额外绑定 */ } - /* ---------------- 主题(MD3/MIUIx × 深/浅) ---------------- */ - function applyTheme(style, theme) { - state.style = style; state.theme = theme; + /* ---------------- 主题(MD3/MIUIx × 明/暗) ---------------- + * data-style(md3/miuix)× data-mode(dark/light),四套 CSS 变量表驱动。 + * 默认值联动设备 settings.conf(ui.style / display.theme,经 GET /api/theme 读取); + * 面板内选择存 localStorage,作为本面板覆盖(不写设备): + * meowpanel.style = md3|miuix (缺省 = 跟随设备 ui.style) + * meowpanel.mode = system|browser|light|dark + * (缺省 system = 跟随设备 display.theme; + * browser = 跟随浏览器 prefers-color-scheme,日/夜切换实时生效) + * 顶栏切换器保留旧行为:本面板生效 + 经 PUT /api/theme 同步设备。 */ + function browserMode() { + try { return (window.matchMedia && matchMedia('(prefers-color-scheme: light)').matches) ? 'light' : 'dark'; } + catch (e) { return 'dark'; } + } + function storedStyle() { const v = storageGet('meowpanel.style'); return (v === 'md3' || v === 'miuix') ? v : null; } + function storedMode() { const v = storageGet('meowpanel.mode'); return (v === 'system' || v === 'browser' || v === 'light' || v === 'dark') ? v : null; } + function resolveStyle() { return storedStyle() || state.devStyle || 'md3'; } + function resolveMode() { + const m = storedMode() || 'system'; + if (m === 'light' || m === 'dark') return m; + if (m === 'system') return state.devTheme || browserMode(); // 设备未知时回退浏览器偏好 + return browserMode(); + } + function devStyleLabel() { return state.devStyle ? (state.devStyle === 'miuix' ? 'MIUIx' : 'Material You') : '未知(后端离线)'; } + function devThemeLabel() { return state.devTheme ? (state.devTheme === 'light' ? '浅色' : '深色') : '未知(后端离线)'; } + function applyTheme() { + const style = resolveStyle(), mode = resolveMode(); + state.style = style; state.mode = mode; const root = document.documentElement; - root.dataset.style = style; root.dataset.theme = theme; + root.dataset.style = style; root.dataset.mode = mode; $$('#style-seg .seg-btn').forEach(b => b.classList.toggle('active', b.dataset.style === style)); - $$('#theme-seg .seg-btn').forEach(b => b.classList.toggle('active', b.dataset.theme === theme)); - try { localStorage.setItem('meowpanel.style', style); localStorage.setItem('meowpanel.theme', theme); } catch (e) {} + $$('#theme-seg .seg-btn').forEach(b => b.classList.toggle('active', b.dataset.mode === mode)); + // 设置页「主题」卡(仅在该页渲染时存在) + $$('#set-style-seg .seg-btn').forEach(b => b.classList.toggle('active', b.dataset.style === style)); + $$('#set-mode-seg .seg-btn').forEach(b => b.classList.toggle('active', b.dataset.mode === (storedMode() || 'system'))); + const rst = $('#set-style-reset'); if (rst) rst.disabled = !storedStyle(); } async function sendTheme(patch) { try { await api('PUT', '/api/theme', patch); } catch (e) { toast('主题已本地切换(后端未同步:' + e.message + ')'); } } async function initTheme() { - let style = 'md3', theme = 'dark'; - try { style = localStorage.getItem('meowpanel.style') || 'md3'; theme = localStorage.getItem('meowpanel.theme') || 'dark'; } catch (e) {} - applyTheme(style, theme); + // 旧版键迁移:meowpanel.theme(dark/light)→ meowpanel.mode + const old = storageGet('meowpanel.theme'); + if (old) { + if (!storedMode()) storageSet('meowpanel.mode', old === 'light' ? 'light' : 'dark'); + storageDel('meowpanel.theme'); + } + applyTheme(); + // 浏览器日/夜切换:模式为 browser(或 system 且设备主题未知)时实时跟随 + try { + if (window.matchMedia) { + const mq = matchMedia('(prefers-color-scheme: dark)'); + const onChange = function () { + const m = storedMode() || 'system'; + if (m === 'browser' || (m === 'system' && !state.devTheme)) applyTheme(); + }; + if (mq.addEventListener) mq.addEventListener('change', onChange); + else if (mq.addListener) mq.addListener(onChange); + } + } catch (e) {} + // 设备默认值(ui.style/display.theme)——未覆盖时的默认来源 try { const t = await api('GET', '/api/theme'); - applyTheme(t.style || style, t.theme || theme); - } catch (e) { /* 后端离线,保持本地 */ } + state.devStyle = (t.style === 'md3' || t.style === 'miuix') ? t.style : null; + state.devTheme = (t.theme === 'light' || t.theme === 'dark') ? t.theme : null; + applyTheme(); + if (state.route === 'settings') renderSettings(); // 刷新「主题」卡的设备当前值提示 + } catch (e) { /* 后端离线,保持本地/浏览器偏好 */ } } - // 顶栏「风格 / 明暗」切换器:本地立即切换 + 同步后端 + // 顶栏「风格 / 明暗」切换器:本面板覆盖(localStorage)+ 同步设备 settings.conf function bindThemeUi() { $('#style-seg').addEventListener('click', function (e) { const b = e.target.closest('.seg-btn'); if (!b) return; - applyTheme(b.dataset.style, state.theme); + storageSet('meowpanel.style', b.dataset.style); + applyTheme(); sendTheme({ style: b.dataset.style }); }); $('#theme-seg').addEventListener('click', function (e) { const b = e.target.closest('.seg-btn'); if (!b) return; - applyTheme(state.style, b.dataset.theme); - sendTheme({ theme: b.dataset.theme }); + storageSet('meowpanel.mode', b.dataset.mode); // 顶栏=显式明/暗 + applyTheme(); + sendTheme({ theme: b.dataset.mode }); }); } @@ -727,6 +777,21 @@ '' + ''; const items = d.items || []; + // 面板外观(主题):本面板覆盖(localStorage),默认跟随设备 ui.style / display.theme + h += '

主题(面板外观)

' + + '
' + + '
' + + '' + + '
' + + '
' + + '

默认跟随设备 ui.style(当前:' + esc(devStyleLabel()) + ');在面板选择即为本面板覆盖,只影响网页观感(顶栏切换才会同步设备)。

' + + '
' + + '
' + + '' + + '' + + '' + + '
' + + '

跟随系统 = 设备 display.theme(当前:' + esc(devThemeLabel()) + ');跟随浏览器 = prefers-color-scheme,随浏览器日/夜自动切换。

'; // 面板鉴权(可选防护)单独一卡 const authItems = items.filter(x => x.key === 'panel.auth_enabled' || x.key === 'panel.auth_pass'); if (authItems.length) { @@ -751,6 +816,7 @@ '

导入 tar.gz(内含 font.png + font.idx + font.json)→ 校验后解压到 MeowOS/fonts/<family>/<weight>/;导出把该三件套打包为 tar.gz。导入后进设备「设置 → 字体」页列表自动重扫生效。

'; h += ''; $('#content').innerHTML = h; + applyTheme(); // 同步「主题」卡 seg 选中态(applyTheme 按 id 查询,不存在时为空操作) loadFonts(); } async function loadFonts() { @@ -803,6 +869,12 @@ else if (t.dataset.stype === 'str') updateSetting(sk, t.value, 'str'); }); c.addEventListener('click', async function (e) { + // 「主题(面板外观)」卡:本面板覆盖(localStorage),不写设备 + const ss = e.target.closest('#set-style-seg .seg-btn'); + if (ss) { storageSet('meowpanel.style', ss.dataset.style); applyTheme(); return; } + const sm = e.target.closest('#set-mode-seg .seg-btn'); + if (sm) { storageSet('meowpanel.mode', sm.dataset.mode); applyTheme(); return; } + if (e.target.closest('#set-style-reset')) { storageDel('meowpanel.style'); applyTheme(); return; } if (e.target.closest('#font-import')) { const inp = $('#font-file'); if (inp) { inp.value = ''; inp.click(); } return; } const fe = e.target.closest('[data-fexport]'); if (fe) {