feat(meowpanel): 面板 MD3/MIUIx 双主题 × 明暗四套变量表 + 主题切换器(跟随系统/浏览器/明/暗)

- 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,无需新增
This commit is contained in:
MeowOS committed 2026-09-01 16:26:02 +08:00
1 parent 250c9ae8b1
commit 677c0c47ac
3 files changed
+144 -39

No files matched your search

+88 -16
View File
@@ -37,7 +37,8 @@
/* ---------------- 全局状态 ---------------- */
const state = {
style: 'md3', theme: 'dark',
style: 'md3', mode: 'dark', // 已解析的风格/明暗(应用到 <html> 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 × 明/暗) ----------------
* <html> 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 @@
'<button class="btn btn-danger" id="set-restore">恢复全部默认</button>' +
'<button class="btn" id="set-refresh">刷新</button></div>';
const items = d.items || [];
// 面板外观(主题):本面板覆盖(localStorage),默认跟随设备 ui.style / display.theme
h += '<div class="card" id="theme-card"><h3 class="card-title">主题(面板外观)</h3>' +
'<div class="field"><label class="field-label">风格</label>' +
'<div class="field-row wrap"><div class="seg" id="set-style-seg">' +
'<button class="seg-btn" data-style="md3">Material You</button>' +
'<button class="seg-btn" data-style="miuix">MIUIx</button></div>' +
'<button class="btn btn-ghost" id="set-style-reset">跟随设备</button></div>' +
'<p class="muted" style="margin-top:6px">默认跟随设备 ui.style(当前:' + esc(devStyleLabel()) + ');在面板选择即为本面板覆盖,只影响网页观感(顶栏切换才会同步设备)。</p></div>' +
'<div class="field" style="margin-bottom:0"><label class="field-label">明暗模式</label>' +
'<div class="seg" id="set-mode-seg">' +
'<button class="seg-btn" data-mode="system">跟随系统</button>' +
'<button class="seg-btn" data-mode="browser">跟随浏览器</button>' +
'<button class="seg-btn" data-mode="light">浅色</button>' +
'<button class="seg-btn" data-mode="dark">深色</button></div>' +
'<p class="muted" style="margin-top:6px">跟随系统 = 设备 display.theme(当前:' + esc(devThemeLabel()) + ');跟随浏览器 = prefers-color-scheme,随浏览器日/夜自动切换。</p></div></div>';
// 面板鉴权(可选防护)单独一卡
const authItems = items.filter(x => x.key === 'panel.auth_enabled' || x.key === 'panel.auth_pass');
if (authItems.length) {
@@ -751,6 +816,7 @@
'<p class="muted" style="margin-top:8px">导入 tar.gz(内含 font.png + font.idx + font.json)→ 校验后解压到 MeowOS/fonts/&lt;family&gt;/&lt;weight&gt;/;导出把该三件套打包为 tar.gz。导入后进设备「设置 → 字体」页列表自动重扫生效。</p></div>';
h += '</div>';
$('#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) {