qq-bot-roost 1.0.9 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/web/app.js +207 -0
- package/web/index.html +122 -51
- package/web/style.css +477 -0
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="零依赖">
|
|
11
11
|
<img src="https://img.shields.io/badge/tests-826%20passed-brightgreen" alt="测试 826 项">
|
|
12
12
|
<img src="https://img.shields.io/badge/plugins-JS%20%7C%20Python%20%7C%20Go%20%7C%20any-blue" alt="多语言插件">
|
|
13
|
-
<img src="https://img.shields.io/badge/version-1.0.
|
|
13
|
+
<img src="https://img.shields.io/badge/version-1.0.10-blueviolet" alt="版本 1.0.10">
|
|
14
14
|
<img src="https://img.shields.io/badge/license-Non--Commercial-red" alt="禁止商用">
|
|
15
15
|
<a href="https://github.com/tdcq9594/qq-bot-roost/actions"><img src="https://img.shields.io/github/actions/workflow/status/tdcq9594/qq-bot-roost/ci.yml?branch=master" alt="CI"></a>
|
|
16
16
|
|
package/package.json
CHANGED
package/web/app.js
CHANGED
|
@@ -606,6 +606,8 @@ function activateTab(name) {
|
|
|
606
606
|
if (name === 'roles') { loadRoles(); loadPeople(); }
|
|
607
607
|
if (name === 'identity') loadIdentity();
|
|
608
608
|
if (name === 'notify') loadNotify();
|
|
609
|
+
// 纯展示同步:侧栏 / 底栏高亮、面包屑、抽屉收起。不碰任何数据加载。
|
|
610
|
+
syncNav(name);
|
|
609
611
|
}
|
|
610
612
|
|
|
611
613
|
/**
|
|
@@ -4483,3 +4485,208 @@ function connectStream() {
|
|
|
4483
4485
|
// 机器人确定后再刷一次:上面那次是抢时间,这次才是准数据
|
|
4484
4486
|
if (initial) activateTab(initial);
|
|
4485
4487
|
})();
|
|
4488
|
+
|
|
4489
|
+
/* ============================================================
|
|
4490
|
+
响应式导航外观(2026-11-06 重构新增)
|
|
4491
|
+
------------------------------------------------------------
|
|
4492
|
+
这一整段只做「壳」:高亮、面包屑、抽屉开合、表格标签、theme-color。
|
|
4493
|
+
不调用任何数据加载函数,不改任何 API、轮询或 iframe 逻辑。
|
|
4494
|
+
============================================================ */
|
|
4495
|
+
|
|
4496
|
+
/**
|
|
4497
|
+
* 页签切换后同步各处导航状态。
|
|
4498
|
+
* 由 activateTab() 末尾调用;切机器人 / hashchange 走的也是 activateTab,
|
|
4499
|
+
* 所以不需要额外注册第二套监听。
|
|
4500
|
+
*/
|
|
4501
|
+
function syncNav(name) {
|
|
4502
|
+
const nav = $('#mainNav');
|
|
4503
|
+
if (!nav) return;
|
|
4504
|
+
const btn = $$('[data-tab]').find(
|
|
4505
|
+
(b) => b.dataset.tab === name && b.classList.contains('tab-item'),
|
|
4506
|
+
);
|
|
4507
|
+
const crumb = $('#navCrumb');
|
|
4508
|
+
if (crumb) {
|
|
4509
|
+
crumb.textContent = btn ? btn.textContent.trim()
|
|
4510
|
+
: (name.startsWith('page:') ? '插件页面' : '概览');
|
|
4511
|
+
}
|
|
4512
|
+
// 停在非高频页时,底栏「更多」给个提示态,免得用户以为没有当前页
|
|
4513
|
+
const more = $('#navMore');
|
|
4514
|
+
if (more) more.classList.toggle('has-active', Boolean(btn) && !btn.hasAttribute('data-primary'));
|
|
4515
|
+
// 切到某个页签时把它所在的折叠组展开,否则用户看不到自己选中了哪一页
|
|
4516
|
+
const group = btn?.closest('.tab-group');
|
|
4517
|
+
if (group?.classList.contains('group-collapsed')) setGroupCollapsed(group, false);
|
|
4518
|
+
// 选完页就把抽屉 / 平板展开态收起来
|
|
4519
|
+
nav.classList.remove('drawer-open', 'expanded');
|
|
4520
|
+
const toggle = $('#navToggle');
|
|
4521
|
+
if (toggle) toggle.setAttribute('aria-expanded', 'false');
|
|
4522
|
+
if (more) more.setAttribute('aria-expanded', 'false');
|
|
4523
|
+
}
|
|
4524
|
+
|
|
4525
|
+
/* 侧栏偏好:纯外观状态,存 localStorage,不参与任何业务数据 */
|
|
4526
|
+
const NAV_COMPACT_KEY = 'roost_nav_compact';
|
|
4527
|
+
const NAV_GROUPS_KEY = 'roost_nav_groups';
|
|
4528
|
+
|
|
4529
|
+
/** 折叠 / 展开一个分组,并记住选择 */
|
|
4530
|
+
function setGroupCollapsed(group, on) {
|
|
4531
|
+
if (!group) return;
|
|
4532
|
+
group.classList.toggle('group-collapsed', on);
|
|
4533
|
+
const key = group.dataset.group;
|
|
4534
|
+
if (!key) return;
|
|
4535
|
+
let list = [];
|
|
4536
|
+
try { list = JSON.parse(localStorage.getItem(NAV_GROUPS_KEY) || '[]'); } catch {}
|
|
4537
|
+
if (!Array.isArray(list)) list = [];
|
|
4538
|
+
const next = on ? [...new Set([...list, key])] : list.filter((x) => x !== key);
|
|
4539
|
+
try { localStorage.setItem(NAV_GROUPS_KEY, JSON.stringify(next)); } catch {}
|
|
4540
|
+
}
|
|
4541
|
+
|
|
4542
|
+
/** 恢复上次的收起 / 折叠状态;当前页所在组强制展开 */
|
|
4543
|
+
function applyNavPrefs() {
|
|
4544
|
+
try {
|
|
4545
|
+
if (localStorage.getItem(NAV_COMPACT_KEY) === '1') {
|
|
4546
|
+
document.body.classList.add('nav-compact');
|
|
4547
|
+
}
|
|
4548
|
+
const list = JSON.parse(localStorage.getItem(NAV_GROUPS_KEY) || '[]');
|
|
4549
|
+
if (Array.isArray(list)) {
|
|
4550
|
+
$$('.app-nav .tab-group').forEach((g) => {
|
|
4551
|
+
if (list.includes(g.dataset.group)) g.classList.add('group-collapsed');
|
|
4552
|
+
});
|
|
4553
|
+
}
|
|
4554
|
+
} catch {}
|
|
4555
|
+
const active = $('.app-nav .tab-item.active')?.closest('.tab-group');
|
|
4556
|
+
if (active) setGroupCollapsed(active, false);
|
|
4557
|
+
}
|
|
4558
|
+
|
|
4559
|
+
/**
|
|
4560
|
+
* 给表格单元格补 data-label,供窄屏卡片化的 td::before 使用。
|
|
4561
|
+
*
|
|
4562
|
+
* 12 张表的行几乎全是 JS 动态渲染的(app.js 里 80 多处 <td 模板),
|
|
4563
|
+
* 逐处改模板既伤业务代码又容易漏。这里用 MutationObserver 在 tbody
|
|
4564
|
+
* 内容变化时按 thead 的列序自动贴标签,纯展示、零业务侵入。
|
|
4565
|
+
* 空状态行(colspan)不贴,避免卡片上出现一个没有值的字段。
|
|
4566
|
+
*/
|
|
4567
|
+
function initTableLabels() {
|
|
4568
|
+
const labelsFor = (table) =>
|
|
4569
|
+
[...table.querySelectorAll('thead th')].map((th) => th.textContent.trim());
|
|
4570
|
+
|
|
4571
|
+
const decorate = (tbody) => {
|
|
4572
|
+
const table = tbody.closest('table');
|
|
4573
|
+
if (!table) return;
|
|
4574
|
+
const labels = labelsFor(table);
|
|
4575
|
+
for (const tr of tbody.rows) {
|
|
4576
|
+
Array.from(tr.cells).forEach((td, i) => {
|
|
4577
|
+
if (td.hasAttribute('colspan') || !labels[i]) return;
|
|
4578
|
+
if (!td.dataset.label) td.dataset.label = labels[i];
|
|
4579
|
+
});
|
|
4580
|
+
}
|
|
4581
|
+
};
|
|
4582
|
+
|
|
4583
|
+
const bodies = $$('table tbody');
|
|
4584
|
+
bodies.forEach(decorate);
|
|
4585
|
+
if (typeof MutationObserver === 'undefined') return;
|
|
4586
|
+
|
|
4587
|
+
const mo = new MutationObserver((records) => {
|
|
4588
|
+
for (const r of records) {
|
|
4589
|
+
const t = r.target;
|
|
4590
|
+
if (!t) continue;
|
|
4591
|
+
if (t.tagName === 'TBODY') decorate(t);
|
|
4592
|
+
else if (t.closest) {
|
|
4593
|
+
const tb = t.closest('tbody');
|
|
4594
|
+
if (tb) decorate(tb);
|
|
4595
|
+
}
|
|
4596
|
+
}
|
|
4597
|
+
});
|
|
4598
|
+
// subtree: 业务若往已有 <tr> 里追加 <td>(不是整块 innerHTML 替换)也要补标签
|
|
4599
|
+
bodies.forEach((tb) => mo.observe(tb, { childList: true, subtree: true }));
|
|
4600
|
+
}
|
|
4601
|
+
|
|
4602
|
+
/** 手机浏览器地址栏跟随深浅色,纯展示,不碰 setTheme() */
|
|
4603
|
+
function initThemeColor() {
|
|
4604
|
+
const meta = document.querySelector('meta[name="theme-color"]');
|
|
4605
|
+
if (!meta) return;
|
|
4606
|
+
const apply = () => {
|
|
4607
|
+
meta.setAttribute(
|
|
4608
|
+
'content',
|
|
4609
|
+
document.documentElement.dataset.theme === 'dark' ? '#19120e' : '#f8f5f1',
|
|
4610
|
+
);
|
|
4611
|
+
};
|
|
4612
|
+
apply();
|
|
4613
|
+
if (typeof MutationObserver === 'undefined') return;
|
|
4614
|
+
new MutationObserver(apply).observe(document.documentElement, {
|
|
4615
|
+
attributes: true,
|
|
4616
|
+
attributeFilter: ['data-theme'],
|
|
4617
|
+
});
|
|
4618
|
+
}
|
|
4619
|
+
|
|
4620
|
+
(function initNavChrome() {
|
|
4621
|
+
const nav = $('#mainNav');
|
|
4622
|
+
if (!nav) return;
|
|
4623
|
+
const toggle = $('#navToggle');
|
|
4624
|
+
const more = $('#navMore');
|
|
4625
|
+
const close = $('#navClose');
|
|
4626
|
+
|
|
4627
|
+
const setOpen = (on) => {
|
|
4628
|
+
// 只挂当前档位对应的 class:同时挂两个,用户拖动窗口跨断点时会残留另一个形态
|
|
4629
|
+
const isPhone = window.matchMedia('(max-width: 720px)').matches;
|
|
4630
|
+
nav.classList.toggle('drawer-open', on && isPhone);
|
|
4631
|
+
nav.classList.toggle('expanded', on && !isPhone);
|
|
4632
|
+
if (toggle) toggle.setAttribute('aria-expanded', String(on));
|
|
4633
|
+
if (more) more.setAttribute('aria-expanded', String(on));
|
|
4634
|
+
};
|
|
4635
|
+
|
|
4636
|
+
toggle?.addEventListener('click', (e) => {
|
|
4637
|
+
e.stopPropagation();
|
|
4638
|
+
if (window.matchMedia('(max-width: 720px)').matches) {
|
|
4639
|
+
// 手机:打开全量抽屉
|
|
4640
|
+
setOpen(!nav.classList.contains('drawer-open'));
|
|
4641
|
+
} else if (window.matchMedia('(min-width: 721px) and (max-width: 1023px)').matches) {
|
|
4642
|
+
// 平板:展开 / 收回 60px 图标条
|
|
4643
|
+
setOpen(!nav.classList.contains('expanded'));
|
|
4644
|
+
} else {
|
|
4645
|
+
// 桌面:收起 / 展开侧栏,记住选择
|
|
4646
|
+
const compact = !document.body.classList.contains('nav-compact');
|
|
4647
|
+
document.body.classList.toggle('nav-compact', compact);
|
|
4648
|
+
try { localStorage.setItem(NAV_COMPACT_KEY, compact ? '1' : '0'); } catch {}
|
|
4649
|
+
toggle.setAttribute('aria-expanded', String(!compact));
|
|
4650
|
+
toggle.title = compact ? '展开侧栏' : '收起侧栏';
|
|
4651
|
+
}
|
|
4652
|
+
});
|
|
4653
|
+
|
|
4654
|
+
// 分组标题点击 = 折叠 / 展开下级菜单(snowluma 那种手风琴)
|
|
4655
|
+
$$('.app-nav .tab-parent').forEach((p) => {
|
|
4656
|
+
p.addEventListener('click', (e) => {
|
|
4657
|
+
e.stopPropagation();
|
|
4658
|
+
const g = p.closest('.tab-group');
|
|
4659
|
+
if (!g) return;
|
|
4660
|
+
// 收起态下点分组图标 = 「我想看这一组」,直接展开侧栏并展开该组
|
|
4661
|
+
if (document.body.classList.contains('nav-compact')
|
|
4662
|
+
&& window.matchMedia('(min-width: 1024px)').matches) {
|
|
4663
|
+
document.body.classList.remove('nav-compact');
|
|
4664
|
+
try { localStorage.setItem(NAV_COMPACT_KEY, '0'); } catch {}
|
|
4665
|
+
setGroupCollapsed(g, false);
|
|
4666
|
+
return;
|
|
4667
|
+
}
|
|
4668
|
+
setGroupCollapsed(g, !g.classList.contains('group-collapsed'));
|
|
4669
|
+
});
|
|
4670
|
+
});
|
|
4671
|
+
|
|
4672
|
+
more?.addEventListener('click', (e) => {
|
|
4673
|
+
e.stopPropagation();
|
|
4674
|
+
setOpen(true);
|
|
4675
|
+
});
|
|
4676
|
+
close?.addEventListener('click', (e) => {
|
|
4677
|
+
e.stopPropagation();
|
|
4678
|
+
setOpen(false);
|
|
4679
|
+
});
|
|
4680
|
+
document.addEventListener('keydown', (e) => {
|
|
4681
|
+
if (e.key === 'Escape') setOpen(false);
|
|
4682
|
+
});
|
|
4683
|
+
|
|
4684
|
+
// 初始化:先恢复偏好,再补一次外观同步(无 hash 时 activateTab 不会被调用)
|
|
4685
|
+
applyNavPrefs();
|
|
4686
|
+
if (toggle) {
|
|
4687
|
+
toggle.title = document.body.classList.contains('nav-compact') ? '展开侧栏' : '收起侧栏';
|
|
4688
|
+
}
|
|
4689
|
+
syncNav(location.hash.slice(1) || 'overview');
|
|
4690
|
+
initTableLabels();
|
|
4691
|
+
initThemeColor();
|
|
4692
|
+
})();
|
package/web/index.html
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
<html lang="zh-CN">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
6
|
+
<meta name="theme-color" content="#f8f5f1">
|
|
6
7
|
<title>栖 Roost · 控制台</title>
|
|
7
8
|
<!-- 主题脚本必须外置:CSP 的 script-src 'self' 会拦掉内联脚本 -->
|
|
8
9
|
<script src="/theme-boot.js"></script>
|
|
@@ -11,6 +12,11 @@
|
|
|
11
12
|
<body>
|
|
12
13
|
|
|
13
14
|
<header class="topbar">
|
|
15
|
+
<button class="nav-toggle" id="navToggle" aria-label="打开导航" aria-expanded="false">
|
|
16
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
|
17
|
+
<path d="M4 7h16M4 12h16M4 17h16"/>
|
|
18
|
+
</svg>
|
|
19
|
+
</button>
|
|
14
20
|
<div class="brand" title="栖 Roost · 倦飞之鸟,终有一栖">
|
|
15
21
|
<span class="logo" aria-hidden="true">栖</span>
|
|
16
22
|
<div>
|
|
@@ -42,67 +48,132 @@
|
|
|
42
48
|
</div>
|
|
43
49
|
</div>
|
|
44
50
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
<!--
|
|
52
|
+
导航:三档布局共用这一份 DOM,靠 CSS 变形(侧栏 / 图标条 / 底部导航 + 抽屉)。
|
|
53
|
+
不要复制出第二套 [data-tab] 按钮 —— activateTab() 用 `b === btn` 高亮,
|
|
54
|
+
重复 data-tab 会导致只有第一处亮。
|
|
55
|
+
.tab-group / .tab-parent / .tab-menu 三件套和 #pluginPageItems 是
|
|
56
|
+
closeTabMenus()、loadPluginPages() 的硬依赖,类名与 id 一律不能改。
|
|
57
|
+
-->
|
|
58
|
+
<nav class="app-nav" id="mainNav" aria-label="主导航">
|
|
59
|
+
<div class="nav-head">
|
|
60
|
+
<span class="nav-brand">栖 <span>Roost</span></span>
|
|
61
|
+
<button class="nav-close" id="navClose" aria-label="收起导航">
|
|
62
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
|
63
|
+
<path d="M6 6l12 12M18 6L6 18"/>
|
|
54
64
|
</svg>
|
|
55
65
|
</button>
|
|
56
|
-
<div class="tab-menu" hidden>
|
|
57
|
-
<button class="tab-item" data-tab="stats">统计</button>
|
|
58
|
-
<button class="tab-item" data-tab="events">事件</button>
|
|
59
|
-
<button class="tab-item" data-tab="reach">可达性</button>
|
|
60
|
-
<button class="tab-item" data-tab="delivery">投递</button>
|
|
61
|
-
<button class="tab-item" data-tab="ledger">账本</button>
|
|
62
|
-
<button class="tab-item" data-tab="logs">日志</button>
|
|
63
|
-
</div>
|
|
64
66
|
</div>
|
|
65
67
|
|
|
66
|
-
<div class="
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
68
|
+
<div class="nav-scroll">
|
|
69
|
+
<div class="tab-group" data-group="run">
|
|
70
|
+
<button class="tab tab-parent" data-icon="运" aria-label="运行" aria-haspopup="true" aria-expanded="false">
|
|
71
|
+
<svg class="nav-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
72
|
+
<rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/>
|
|
73
|
+
<rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/>
|
|
74
|
+
</svg>
|
|
75
|
+
<span class="nav-label">运行</span>
|
|
76
|
+
<svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
77
|
+
stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
|
78
|
+
<path d="M6 9l6 6 6-6"/>
|
|
79
|
+
</svg>
|
|
80
|
+
</button>
|
|
81
|
+
<div class="tab-menu" hidden>
|
|
82
|
+
<button class="tab-item active" data-tab="overview" data-primary>概览</button>
|
|
83
|
+
<button class="tab-item" data-tab="stats">统计</button>
|
|
84
|
+
<button class="tab-item" data-tab="logs" data-primary>日志</button>
|
|
85
|
+
</div>
|
|
78
86
|
</div>
|
|
79
|
-
</div>
|
|
80
87
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
<div class="tab-menu
|
|
93
|
-
|
|
94
|
-
|
|
88
|
+
<div class="tab-group" data-group="diagnose">
|
|
89
|
+
<button class="tab tab-parent" data-icon="诊" aria-label="诊断" aria-haspopup="true" aria-expanded="false">
|
|
90
|
+
<svg class="nav-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
91
|
+
<path d="M2 12h4l3-7 4 14 3-7h6"/>
|
|
92
|
+
</svg>
|
|
93
|
+
<span class="nav-label">诊断</span>
|
|
94
|
+
<svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
95
|
+
stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
|
96
|
+
<path d="M6 9l6 6 6-6"/>
|
|
97
|
+
</svg>
|
|
98
|
+
</button>
|
|
99
|
+
<div class="tab-menu" hidden>
|
|
100
|
+
<button class="tab-item" data-tab="events">事件</button>
|
|
101
|
+
<button class="tab-item" data-tab="reach">可达性</button>
|
|
102
|
+
<button class="tab-item" data-tab="delivery">投递</button>
|
|
103
|
+
<button class="tab-item" data-tab="ledger">账本</button>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div class="tab-group" data-group="manage">
|
|
108
|
+
<button class="tab tab-parent" data-icon="管" aria-label="管理" aria-haspopup="true" aria-expanded="false">
|
|
109
|
+
<svg class="nav-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
110
|
+
<path d="M4 6h9M17 6h3M4 12h3M11 12h9M4 18h9M17 18h3"/>
|
|
111
|
+
<circle cx="15" cy="6" r="2"/><circle cx="9" cy="12" r="2"/><circle cx="15" cy="18" r="2"/>
|
|
112
|
+
</svg>
|
|
113
|
+
<span class="nav-label">管理</span>
|
|
114
|
+
<svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
115
|
+
stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
|
116
|
+
<path d="M6 9l6 6 6-6"/>
|
|
117
|
+
</svg>
|
|
118
|
+
</button>
|
|
119
|
+
<div class="tab-menu" hidden>
|
|
120
|
+
<button class="tab-item" data-tab="config" data-primary>配置</button>
|
|
121
|
+
<!-- 身份:QQ号 / 群号 ↔ openid。常用页而不是设置项 -->
|
|
122
|
+
<button class="tab-item" data-tab="identity">身份</button>
|
|
123
|
+
<button class="tab-item" data-tab="groups">群组</button>
|
|
124
|
+
<button class="tab-item" data-tab="roles">权限</button>
|
|
125
|
+
<!-- 通知:SMTP 邮箱配置 + 普通成员自助绑定 -->
|
|
126
|
+
<button class="tab-item" data-tab="notify">通知</button>
|
|
127
|
+
</div>
|
|
95
128
|
</div>
|
|
96
|
-
</div>
|
|
97
129
|
|
|
98
|
-
|
|
99
|
-
|
|
130
|
+
<div class="tab-group" data-group="plugins">
|
|
131
|
+
<button class="tab tab-parent" data-icon="扩" aria-label="扩展" aria-haspopup="true" aria-expanded="false">
|
|
132
|
+
<svg class="nav-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
133
|
+
<path d="M4 4h6v2a2 2 0 1 0 4 0V4h6v6h-2a2 2 0 1 0 0 4h2v6h-6v-2a2 2 0 1 0-4 0v2H4v-6h2a2 2 0 1 0 0-4H4V4z"/>
|
|
134
|
+
</svg>
|
|
135
|
+
<span class="nav-label">扩展</span>
|
|
136
|
+
<svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
137
|
+
stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
|
138
|
+
<path d="M6 9l6 6 6-6"/>
|
|
139
|
+
</svg>
|
|
140
|
+
</button>
|
|
141
|
+
<div class="tab-menu" hidden>
|
|
142
|
+
<button class="tab-item" data-tab="plugins" data-primary>插件管理</button>
|
|
143
|
+
<!-- 插件注册的页面挂到这里,见 loadPluginPages() -->
|
|
144
|
+
<div class="tab-menu-sep" id="pluginPageSep" hidden></div>
|
|
145
|
+
<div class="tab-menu-label" id="pluginPageLabel" hidden>插件页面</div>
|
|
146
|
+
<div class="tab-menu-sub" id="pluginPageItems"></div>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
100
149
|
|
|
101
|
-
|
|
102
|
-
|
|
150
|
+
<div class="tab-group" data-group="system">
|
|
151
|
+
<button class="tab tab-parent" data-icon="系" aria-label="系统" aria-haspopup="true" aria-expanded="false">
|
|
152
|
+
<svg class="nav-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
153
|
+
<circle cx="12" cy="12" r="9"/><path d="M12 11v6M12 7.5h.01"/>
|
|
154
|
+
</svg>
|
|
155
|
+
<span class="nav-label">系统</span>
|
|
156
|
+
<svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
157
|
+
stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
|
158
|
+
<path d="M6 9l6 6 6-6"/>
|
|
159
|
+
</svg>
|
|
160
|
+
</button>
|
|
161
|
+
<div class="tab-menu" hidden>
|
|
162
|
+
<button class="tab-item" data-tab="about">关于</button>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
103
166
|
|
|
104
|
-
<button class="
|
|
167
|
+
<button class="nav-more" id="navMore" aria-label="全部功能" aria-expanded="false">
|
|
168
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
|
169
|
+
<circle cx="5" cy="12" r="1.4"/><circle cx="12" cy="12" r="1.4"/><circle cx="19" cy="12" r="1.4"/>
|
|
170
|
+
</svg>
|
|
171
|
+
<span>更多</span>
|
|
172
|
+
</button>
|
|
105
173
|
</nav>
|
|
174
|
+
|
|
175
|
+
<div class="topbar-spacer"></div>
|
|
176
|
+
<div class="crumb" id="navCrumb">概览</div>
|
|
106
177
|
<button class="icon-btn" id="btnTheme" title="切换日间 / 夜间" aria-label="切换主题">
|
|
107
178
|
<svg class="ic-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
108
179
|
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
package/web/style.css
CHANGED
|
@@ -1425,3 +1425,480 @@ label.switch.small { font-size: 12.5px; color: var(--muted); }
|
|
|
1425
1425
|
white-space: nowrap;
|
|
1426
1426
|
overflow-wrap: anywhere;
|
|
1427
1427
|
}
|
|
1428
|
+
|
|
1429
|
+
/* ============================================================
|
|
1430
|
+
响应式布局重构(2026-11-06)
|
|
1431
|
+
------------------------------------------------------------
|
|
1432
|
+
三档共用同一份导航 DOM,靠 CSS 变形:
|
|
1433
|
+
≥1024 固定分组侧栏
|
|
1434
|
+
721–1023 60px 图标条(可展开)
|
|
1435
|
+
≤720 底部高频 4 项 + 全量抽屉
|
|
1436
|
+
规则一律放文件末尾、用 .app-nav 提高特异性覆盖旧 .tabs 规则,
|
|
1437
|
+
不改 :root 变量名 —— server.js 的 PAGE_THEME 镜像着同一套变量名,
|
|
1438
|
+
改名会让插件页面深浅色失效。
|
|
1439
|
+
============================================================ */
|
|
1440
|
+
|
|
1441
|
+
/* ---------- 顶栏新增件 ---------- */
|
|
1442
|
+
.topbar-spacer { flex: 1; min-width: 0; }
|
|
1443
|
+
.crumb { font-size: 13px; color: var(--muted); white-space: nowrap; }
|
|
1444
|
+
.crumb::before { content: '/'; margin-right: 8px; color: var(--faint); }
|
|
1445
|
+
.nav-toggle {
|
|
1446
|
+
display: none; flex: none; place-items: center;
|
|
1447
|
+
width: 40px; height: 40px; padding: 9px;
|
|
1448
|
+
border: 1px solid var(--border); border-radius: 9px;
|
|
1449
|
+
background: var(--panel); color: var(--muted); cursor: pointer;
|
|
1450
|
+
}
|
|
1451
|
+
.nav-toggle svg { width: 20px; height: 20px; display: block; }
|
|
1452
|
+
.nav-close {
|
|
1453
|
+
display: none; place-items: center;
|
|
1454
|
+
width: 40px; height: 40px; padding: 9px;
|
|
1455
|
+
border: 0; background: none; color: var(--muted); cursor: pointer;
|
|
1456
|
+
}
|
|
1457
|
+
.nav-close svg { width: 20px; height: 20px; display: block; }
|
|
1458
|
+
|
|
1459
|
+
/* ---------- 宽屏:固定分组侧栏 ---------- */
|
|
1460
|
+
.app-nav {
|
|
1461
|
+
position: fixed; top: 60px; left: 0; bottom: 0; width: 240px; z-index: 30;
|
|
1462
|
+
display: flex; flex-direction: column;
|
|
1463
|
+
background: var(--panel); border-right: 1px solid var(--border);
|
|
1464
|
+
overflow: hidden;
|
|
1465
|
+
transition: width .26s var(--ease);
|
|
1466
|
+
}
|
|
1467
|
+
.app-nav .nav-head { display: none; }
|
|
1468
|
+
.app-nav .nav-scroll { flex: 1; min-height: 0; overflow-y: auto; padding: 14px 10px 20px; }
|
|
1469
|
+
/* 滚动条藏起来但保留滚动能力:侧栏那条灰色粗条在窄窗口里很抢眼 */
|
|
1470
|
+
.app-nav .nav-scroll { scrollbar-width: none; -ms-overflow-style: none; }
|
|
1471
|
+
.app-nav .nav-scroll::-webkit-scrollbar { width: 0; height: 0; display: none; }
|
|
1472
|
+
.app-nav .tab-group { position: static; display: block; margin-bottom: 14px; }
|
|
1473
|
+
.app-nav .tab-parent {
|
|
1474
|
+
width: 100%; height: auto; padding: 4px 12px 6px;
|
|
1475
|
+
/* 分组全部折叠后侧栏只剩这 5 行标题,11px 太细、撑不住整列;
|
|
1476
|
+
平板展开态共用这条规则。手机抽屉另有显式 11px 覆盖,不受影响。 */
|
|
1477
|
+
font-size: 13px; font-weight: 600; letter-spacing: .02em;
|
|
1478
|
+
/* 折叠后整列只剩这 5 行标题,--faint 太糊;提到 --muted,仍比菜单项 --text-2 弱一档 */
|
|
1479
|
+
color: var(--muted); cursor: pointer; gap: 7px;
|
|
1480
|
+
/* 必须显式左对齐:平板 / 收起态会把 justify-content 改成 center,
|
|
1481
|
+
切回宽屏时若没覆盖,分组标题会飘到中间,和菜单项错位 */
|
|
1482
|
+
justify-content: flex-start;
|
|
1483
|
+
}
|
|
1484
|
+
.app-nav .tab-parent::after { display: none; }
|
|
1485
|
+
/* 收起态用矢量图标,不再用分组首字 */
|
|
1486
|
+
.app-nav .tab-parent .nav-ico {
|
|
1487
|
+
display: none; width: 18px; height: 18px; flex: none; opacity: 0;
|
|
1488
|
+
transition: opacity .2s var(--ease);
|
|
1489
|
+
}
|
|
1490
|
+
.app-nav .tab-parent .nav-label {
|
|
1491
|
+
flex: 0 1 auto; min-width: 0; max-width: 120px; opacity: 1;
|
|
1492
|
+
white-space: nowrap; overflow: hidden;
|
|
1493
|
+
transition: max-width .26s var(--ease), opacity .18s var(--ease);
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
/*
|
|
1497
|
+
* 桌面分组常驻展开。closeTabMenus() 会给 .tab-menu 设 hidden=true,
|
|
1498
|
+
* 这里用更高特异性覆盖,避免为了「常驻」去改业务侧的下拉开合逻辑。
|
|
1499
|
+
* 代价仅是 aria-expanded 语义不准,不影响任何功能。
|
|
1500
|
+
*/
|
|
1501
|
+
.app-nav .tab-menu,
|
|
1502
|
+
.app-nav .tab-menu[hidden] {
|
|
1503
|
+
display: block; position: static; min-width: 0; padding: 0;
|
|
1504
|
+
border: 0; border-radius: 0; box-shadow: none; animation: none;
|
|
1505
|
+
max-height: 1600px; opacity: 1; overflow: hidden;
|
|
1506
|
+
transition: max-height .26s var(--ease), opacity .18s var(--ease);
|
|
1507
|
+
}
|
|
1508
|
+
/* 插件页面可能很多,给个上限,别让整个分组把侧栏撑到天上去 */
|
|
1509
|
+
.app-nav .tab-menu-sub { max-height: min(52vh, 420px); overflow-y: auto; }
|
|
1510
|
+
.app-nav .tab-item {
|
|
1511
|
+
display: block; width: 100%; text-align: left;
|
|
1512
|
+
min-height: 38px; padding: 8px 12px; margin: 1px 0;
|
|
1513
|
+
border-radius: 8px; font-size: 13px; color: var(--text-2);
|
|
1514
|
+
}
|
|
1515
|
+
.app-nav .tab-item:hover { background: var(--panel-2); color: var(--text); }
|
|
1516
|
+
.app-nav .tab-item.active { background: var(--primary-soft); color: var(--primary); font-weight: 600; }
|
|
1517
|
+
.app-nav .tab-menu-label { padding: 6px 10px 2px; }
|
|
1518
|
+
.app-nav .nav-more { display: none; }
|
|
1519
|
+
|
|
1520
|
+
main {
|
|
1521
|
+
margin-left: 240px; max-width: none; padding: 24px 32px 48px;
|
|
1522
|
+
transition: margin-left .26s var(--ease);
|
|
1523
|
+
}
|
|
1524
|
+
main > .panel { max-width: 1400px; margin-inline: auto; }
|
|
1525
|
+
|
|
1526
|
+
/* ---------- 平板:60px 图标条,汉堡可展开 ---------- */
|
|
1527
|
+
@media (min-width: 721px) and (max-width: 1023px) {
|
|
1528
|
+
.nav-toggle { display: grid; }
|
|
1529
|
+
.app-nav { width: 60px; }
|
|
1530
|
+
.app-nav .nav-scroll { padding: 10px 6px; }
|
|
1531
|
+
.app-nav .tab-group { margin-bottom: 6px; }
|
|
1532
|
+
.app-nav .tab-parent { height: 44px; padding: 0; justify-content: center; color: var(--muted); }
|
|
1533
|
+
.app-nav .tab-parent .nav-ico { display: block; opacity: 1; }
|
|
1534
|
+
.app-nav .tab-parent .nav-label { max-width: 0; opacity: 0; }
|
|
1535
|
+
.app-nav .tab-parent .chev { display: none; }
|
|
1536
|
+
.app-nav .tab-menu,
|
|
1537
|
+
.app-nav .tab-menu[hidden] { max-height: 0; opacity: 0; pointer-events: none; }
|
|
1538
|
+
main { margin-left: 60px; padding: 20px 20px 40px; }
|
|
1539
|
+
|
|
1540
|
+
.app-nav.expanded { width: 240px; box-shadow: var(--shadow-lg); }
|
|
1541
|
+
.app-nav.expanded .tab-parent { height: auto; padding: 2px 10px 6px; justify-content: flex-start; }
|
|
1542
|
+
.app-nav.expanded .tab-parent .nav-label { max-width: 120px; opacity: 1; }
|
|
1543
|
+
.app-nav.expanded .tab-parent .chev { display: block; }
|
|
1544
|
+
.app-nav.expanded .tab-menu,
|
|
1545
|
+
.app-nav.expanded .tab-menu[hidden] { max-height: 1600px; opacity: 1; pointer-events: auto; }
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
/* ---------- 手机:底部导航 + 全量抽屉 ---------- */
|
|
1549
|
+
@media (max-width: 720px) {
|
|
1550
|
+
.topbar { height: 54px; padding: 0 10px; gap: 8px; flex-wrap: nowrap; }
|
|
1551
|
+
.nav-toggle { display: grid; width: 42px; height: 42px; padding: 10px; }
|
|
1552
|
+
.brand { display: none; }
|
|
1553
|
+
.bot-switch { flex: 1; min-width: 0; }
|
|
1554
|
+
.bot-trigger { width: 100%; }
|
|
1555
|
+
.bot-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1556
|
+
.topbar-spacer, .crumb { display: none; }
|
|
1557
|
+
.icon-btn { width: 40px; height: 40px; }
|
|
1558
|
+
.status-pill { padding: 8px 10px; }
|
|
1559
|
+
.status-pill #connText, .status-pill .pill-sep, .status-pill #connAction { display: none; }
|
|
1560
|
+
|
|
1561
|
+
.app-nav {
|
|
1562
|
+
top: auto; bottom: 0; left: 0; right: 0; width: auto;
|
|
1563
|
+
height: calc(58px + env(safe-area-inset-bottom));
|
|
1564
|
+
flex-direction: row; align-items: stretch; z-index: 40;
|
|
1565
|
+
border-right: 0; border-top: 1px solid var(--border);
|
|
1566
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
1567
|
+
}
|
|
1568
|
+
.app-nav .nav-head { display: none; }
|
|
1569
|
+
.app-nav .nav-scroll {
|
|
1570
|
+
display: flex; flex: 1; min-width: 0; align-items: stretch;
|
|
1571
|
+
padding: 0; overflow: visible;
|
|
1572
|
+
}
|
|
1573
|
+
/* display:contents 让分散在 5 个分组里的高频项直接参与底栏 flex */
|
|
1574
|
+
.app-nav .tab-group,
|
|
1575
|
+
.app-nav .tab-menu,
|
|
1576
|
+
.app-nav .tab-menu[hidden] {
|
|
1577
|
+
display: contents;
|
|
1578
|
+
max-height: none; opacity: 1; overflow: visible; pointer-events: auto;
|
|
1579
|
+
}
|
|
1580
|
+
.app-nav .tab-parent,
|
|
1581
|
+
.app-nav .tab-menu-sep,
|
|
1582
|
+
.app-nav .tab-menu-label,
|
|
1583
|
+
.app-nav .tab-menu-sub { display: none; }
|
|
1584
|
+
.app-nav .tab-item:not([data-primary]) { display: none; }
|
|
1585
|
+
.app-nav .tab-item[data-primary] {
|
|
1586
|
+
display: flex; flex: 1 1 0; min-width: 0; min-height: 0; height: auto;
|
|
1587
|
+
align-items: center; justify-content: center;
|
|
1588
|
+
padding: 6px 2px; margin: 0; border-radius: 0;
|
|
1589
|
+
font-size: 12px; text-align: center;
|
|
1590
|
+
transition: color .18s var(--ease), box-shadow .18s var(--ease);
|
|
1591
|
+
}
|
|
1592
|
+
.app-nav .tab-item[data-primary].active {
|
|
1593
|
+
background: transparent; color: var(--primary);
|
|
1594
|
+
box-shadow: inset 0 2px 0 var(--primary);
|
|
1595
|
+
}
|
|
1596
|
+
.app-nav .nav-more {
|
|
1597
|
+
display: flex; flex: 0 0 58px; flex-direction: column;
|
|
1598
|
+
align-items: center; justify-content: center; gap: 2px;
|
|
1599
|
+
border: 0; background: none; color: var(--muted);
|
|
1600
|
+
font: inherit; font-size: 11px; cursor: pointer;
|
|
1601
|
+
}
|
|
1602
|
+
.app-nav .nav-more svg { width: 18px; height: 18px; }
|
|
1603
|
+
main { margin-left: 0; padding: 14px 12px calc(74px + env(safe-area-inset-bottom)); }
|
|
1604
|
+
main > .panel { max-width: none; }
|
|
1605
|
+
|
|
1606
|
+
/*
|
|
1607
|
+
* .card .v 原本是 white-space:nowrap(宽屏好看),但手机 2 列网格里
|
|
1608
|
+
* grid item 的 min-width:auto 会被「undefined MB」「凭证有效期」这类长值撑开,
|
|
1609
|
+
* 整页 scrollWidth 变大,顶栏右侧按钮和底栏「更多」一起被挤出视口。
|
|
1610
|
+
* 必须 minmax(0,1fr) + 允许换行,双管齐下。
|
|
1611
|
+
*/
|
|
1612
|
+
.cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
1613
|
+
.cards > .card { min-width: 0; }
|
|
1614
|
+
.card .v { white-space: normal; overflow-wrap: anywhere; font-size: 17px; }
|
|
1615
|
+
.topbar, .app-nav { max-width: 100vw; }
|
|
1616
|
+
.status-pill { flex: none; }
|
|
1617
|
+
.app-nav .nav-scroll { min-width: 0; overflow: hidden; }
|
|
1618
|
+
|
|
1619
|
+
/* 抽屉:点「更多」后铺满全屏,列出全部分组 */
|
|
1620
|
+
.app-nav.drawer-open {
|
|
1621
|
+
top: 0; height: 100dvh; flex-direction: column;
|
|
1622
|
+
}
|
|
1623
|
+
.app-nav.drawer-open .nav-head {
|
|
1624
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
1625
|
+
padding: 10px 12px; border-bottom: 1px solid var(--border);
|
|
1626
|
+
}
|
|
1627
|
+
.nav-brand { font-family: var(--display); font-size: 16px; font-weight: 700; color: var(--text); }
|
|
1628
|
+
.nav-brand span { font-family: var(--sans); font-size: 10px; letter-spacing: .16em; color: var(--muted); }
|
|
1629
|
+
.app-nav.drawer-open .nav-scroll { display: block; overflow-y: auto; padding: 12px 12px 20px; }
|
|
1630
|
+
.app-nav.drawer-open .tab-group,
|
|
1631
|
+
.app-nav.drawer-open .tab-menu,
|
|
1632
|
+
.app-nav.drawer-open .tab-menu[hidden] {
|
|
1633
|
+
display: block; position: static;
|
|
1634
|
+
max-height: none; opacity: 1; overflow: visible; pointer-events: auto;
|
|
1635
|
+
}
|
|
1636
|
+
.app-nav.drawer-open .tab-parent {
|
|
1637
|
+
display: flex; height: auto; padding: 6px 10px;
|
|
1638
|
+
font-size: 11px; color: var(--faint);
|
|
1639
|
+
}
|
|
1640
|
+
.app-nav.drawer-open .tab-parent .nav-ico {
|
|
1641
|
+
display: block; opacity: 1; width: 16px; height: 16px;
|
|
1642
|
+
}
|
|
1643
|
+
.app-nav.drawer-open .tab-parent .nav-label { max-width: 120px; opacity: 1; }
|
|
1644
|
+
.app-nav.drawer-open .tab-item:not([data-primary]),
|
|
1645
|
+
.app-nav.drawer-open .tab-item[data-primary] {
|
|
1646
|
+
display: block; width: 100%; min-height: 44px; padding: 10px 12px;
|
|
1647
|
+
border-radius: 8px; font-size: 14px; text-align: left;
|
|
1648
|
+
}
|
|
1649
|
+
.app-nav.drawer-open .tab-item[data-primary].active { box-shadow: none; background: var(--primary-soft); }
|
|
1650
|
+
.app-nav.drawer-open .tab-menu-sep,
|
|
1651
|
+
.app-nav.drawer-open .tab-menu-label,
|
|
1652
|
+
.app-nav.drawer-open .tab-menu-sub { display: block; }
|
|
1653
|
+
.app-nav.drawer-open .nav-more { display: none; }
|
|
1654
|
+
.nav-close { display: grid; }
|
|
1655
|
+
|
|
1656
|
+
/* ---------- 12 张表全部卡片化 ---------- */
|
|
1657
|
+
.table-wrap { max-height: none; overflow: visible; }
|
|
1658
|
+
table, #tab-ledger table { min-width: 0; width: 100%; table-layout: auto; }
|
|
1659
|
+
thead { display: none; }
|
|
1660
|
+
tbody, tbody tr { display: block; }
|
|
1661
|
+
tbody tr {
|
|
1662
|
+
margin-bottom: 10px; padding: 10px 12px;
|
|
1663
|
+
background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
|
|
1664
|
+
}
|
|
1665
|
+
tbody td {
|
|
1666
|
+
display: flex; gap: 10px; align-items: baseline;
|
|
1667
|
+
padding: 4px 0; border: 0; text-align: left;
|
|
1668
|
+
}
|
|
1669
|
+
tbody td::before {
|
|
1670
|
+
content: attr(data-label);
|
|
1671
|
+
flex: 0 0 92px; color: var(--muted); font-size: 12px;
|
|
1672
|
+
}
|
|
1673
|
+
tbody td[colspan]:not(.empty)::before { content: none; }
|
|
1674
|
+
tbody td[colspan] { display: block; text-align: center; padding: 18px 0; }
|
|
1675
|
+
tbody td.num { text-align: left; }
|
|
1676
|
+
/* 账本页原本写死列宽且 table-layout:fixed,窄屏必须一并复位 */
|
|
1677
|
+
#tab-ledger .lg-chat, #tab-ledger .lg-kind, #tab-ledger .lg-num,
|
|
1678
|
+
#tab-ledger .lg-range, #tab-ledger .lg-ops, #tab-ledger .lg-when,
|
|
1679
|
+
#tab-ledger .lg-who {
|
|
1680
|
+
width: auto; white-space: normal; overflow: visible; text-overflow: clip;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
/* ---------- 弹窗改全屏 sheet ---------- */
|
|
1684
|
+
.modal-mask { align-items: flex-end; }
|
|
1685
|
+
.modal, .modal.wide {
|
|
1686
|
+
width: 100%; max-width: none; max-height: 92dvh;
|
|
1687
|
+
border-radius: 16px 16px 0 0;
|
|
1688
|
+
padding: 18px 16px calc(18px + env(safe-area-inset-bottom));
|
|
1689
|
+
}
|
|
1690
|
+
.pp-body { max-height: none; flex: 1 1 auto; }
|
|
1691
|
+
.modal-actions { flex-wrap: wrap; }
|
|
1692
|
+
.modal-actions > .btn { min-width: 0; flex: 1 1 auto; }
|
|
1693
|
+
.modal-actions > .act-left { margin-right: 0; }
|
|
1694
|
+
.plugin-page { height: calc(100dvh - 170px); min-height: 320px; }
|
|
1695
|
+
.sticky-actions { bottom: calc(58px + env(safe-area-inset-bottom)); }
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
/* ---------- 触摸设备:达标触控目标(参考 snowluma 的 pointer:coarse 分支) ---------- */
|
|
1699
|
+
@media (hover: none) and (pointer: coarse) {
|
|
1700
|
+
.icon-btn, .nav-toggle { width: 44px; height: 44px; }
|
|
1701
|
+
.app-nav .tab-item, .pp-tab, .btn.small { min-height: 44px; }
|
|
1702
|
+
.chip-x { min-width: 36px; min-height: 44px; }
|
|
1703
|
+
.status-pill { min-height: 40px; }
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
/* ---------- 减少动效 ---------- */
|
|
1707
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1708
|
+
*, *::before, *::after {
|
|
1709
|
+
animation-duration: .01ms !important;
|
|
1710
|
+
transition-duration: .01ms !important;
|
|
1711
|
+
scroll-behavior: auto !important;
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
/* ============================================================
|
|
1716
|
+
侧栏交互(2026-11-07):收起 / 分组折叠
|
|
1717
|
+
------------------------------------------------------------
|
|
1718
|
+
顶栏汉堡在三档里的职责:
|
|
1719
|
+
≥1024 收起 / 展开侧栏(状态存 localStorage)
|
|
1720
|
+
721–1023 展开 / 收回图标条
|
|
1721
|
+
≤720 打开全量抽屉
|
|
1722
|
+
============================================================ */
|
|
1723
|
+
|
|
1724
|
+
/* 汉堡在桌面也常驻,作为「收起侧栏」开关 */
|
|
1725
|
+
.nav-toggle { display: grid; }
|
|
1726
|
+
|
|
1727
|
+
/* 分组标题可点,chevron 指示折叠态 */
|
|
1728
|
+
.app-nav .tab-parent { cursor: pointer; }
|
|
1729
|
+
.app-nav .tab-group .tab-parent .chev {
|
|
1730
|
+
display: block; width: 12px; height: 12px; opacity: .55;
|
|
1731
|
+
margin-left: auto;
|
|
1732
|
+
transform: rotate(180deg);
|
|
1733
|
+
transition: transform .18s var(--ease);
|
|
1734
|
+
}
|
|
1735
|
+
.app-nav .tab-group.group-collapsed .tab-parent .chev { transform: rotate(0deg); }
|
|
1736
|
+
|
|
1737
|
+
/*
|
|
1738
|
+
* 折叠态只在侧栏形态生效;手机抽屉始终展开全部分组,
|
|
1739
|
+
* 免得在抽屉里还要先点开分组才能找到页签。
|
|
1740
|
+
*/
|
|
1741
|
+
@media (min-width: 721px) {
|
|
1742
|
+
.app-nav .tab-group.group-collapsed .tab-menu,
|
|
1743
|
+
.app-nav .tab-group.group-collapsed .tab-menu[hidden] {
|
|
1744
|
+
max-height: 0; opacity: 0; pointer-events: none;
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
/* 桌面收起:60px 图标条,复用平板那套 data-icon 首字 */
|
|
1749
|
+
@media (min-width: 1024px) {
|
|
1750
|
+
body.nav-compact .app-nav { width: 60px; }
|
|
1751
|
+
body.nav-compact main { margin-left: 60px; }
|
|
1752
|
+
body.nav-compact .app-nav .nav-scroll { padding: 10px 6px; }
|
|
1753
|
+
body.nav-compact .app-nav .tab-group { margin-bottom: 6px; }
|
|
1754
|
+
body.nav-compact .app-nav .tab-parent {
|
|
1755
|
+
height: 44px; padding: 0; justify-content: center; color: var(--muted);
|
|
1756
|
+
}
|
|
1757
|
+
body.nav-compact .app-nav .tab-parent .nav-ico { display: block; opacity: 1; }
|
|
1758
|
+
body.nav-compact .app-nav .tab-parent .nav-label { max-width: 0; opacity: 0; }
|
|
1759
|
+
body.nav-compact .app-nav .tab-parent .chev { display: none; }
|
|
1760
|
+
body.nav-compact .app-nav .tab-menu,
|
|
1761
|
+
body.nav-compact .app-nav .tab-menu[hidden] {
|
|
1762
|
+
max-height: 0; opacity: 0; pointer-events: none;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
/* ---------- 收起 / 折叠的收尾细节 ---------- */
|
|
1767
|
+
|
|
1768
|
+
/* 图标条里没地方放折叠箭头,这两条要写在 chevron 通用规则之后才压得住 */
|
|
1769
|
+
@media (min-width: 721px) and (max-width: 1023px) {
|
|
1770
|
+
.app-nav:not(.expanded) .tab-group .tab-parent .chev { display: none; }
|
|
1771
|
+
}
|
|
1772
|
+
@media (min-width: 1024px) {
|
|
1773
|
+
body.nav-compact .app-nav .tab-group .tab-parent .chev { display: none; }
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
/* 抽屉上滑淡入,别硬邦邦地铺满 */
|
|
1777
|
+
@keyframes drawer-in {
|
|
1778
|
+
from { opacity: .55; transform: translateY(14px); }
|
|
1779
|
+
to { opacity: 1; transform: none; }
|
|
1780
|
+
}
|
|
1781
|
+
.app-nav.drawer-open { animation: drawer-in .24s var(--ease); }
|
|
1782
|
+
|
|
1783
|
+
/* 分组标题暗示可点;折叠态不再压暗(整列只剩标题,压暗会糊成一片,
|
|
1784
|
+
折叠状态由 chevron 的旋转方向指示,不靠变淡) */
|
|
1785
|
+
.app-nav .tab-parent:hover { color: var(--text-2); }
|
|
1786
|
+
|
|
1787
|
+
/* ============================================================
|
|
1788
|
+
过渡打磨(2026-11-07):把「跳变」换成连续运动
|
|
1789
|
+
------------------------------------------------------------
|
|
1790
|
+
只追加动效声明,不改任何尺寸 / 颜色 / 布局数值,也不新增 CSS 变量
|
|
1791
|
+
(契约脚本对 cssVarDefs 做双向比对,多一个变量就会 FAIL)。
|
|
1792
|
+
|
|
1793
|
+
对标(§11 调研 + GitHub 动效规范实测 2026-11-07):
|
|
1794
|
+
· 时长阶梯:hover ≈150ms / 状态切换 200–300ms / drawer·modal·accordion 300–400ms
|
|
1795
|
+
(better-react-web-ui「100/300/500」、skillmd animation.md、motionlint STANDARDS.md)
|
|
1796
|
+
· 曲线只留两条:沿用本文件自带 --ease(强 ease-out,进入 / 展开),
|
|
1797
|
+
drawer / bottom-sheet 用 cubic-bezier(.32,.72,0,1)(iOS 曲线)
|
|
1798
|
+
· 只动 transform / opacity,不再给 height / width 增加动画(布局属性是性能反模式)
|
|
1799
|
+
· 入场 scale 不从 0 起(健康区间 .95–.97),退场比入场快约 20%
|
|
1800
|
+
|
|
1801
|
+
现场三类「生硬」根因:
|
|
1802
|
+
1. 切页是 display:none → block,内容凭空出现;
|
|
1803
|
+
2. 分组折叠用 max-height:1600px → 0,前大半程看不出变化、最后一瞬收完;
|
|
1804
|
+
3. 图标 / 箭头靠 display 切换「啪」地出现,颜色硬切,时长普遍只有 .12–.18s。
|
|
1805
|
+
============================================================ */
|
|
1806
|
+
|
|
1807
|
+
/* 1. 切页:内容轻微上移淡入,不再瞬间闪现。
|
|
1808
|
+
弹窗 DOM 在 </main> 之外、sticky-actions 是 position:sticky,
|
|
1809
|
+
所以动画期间的 transform 不会把 fixed 元素锚到面板上。 */
|
|
1810
|
+
@keyframes panel-in {
|
|
1811
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
1812
|
+
to { opacity: 1; transform: none; }
|
|
1813
|
+
}
|
|
1814
|
+
main > .panel.active {
|
|
1815
|
+
animation: panel-in .3s var(--ease) both;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
/* 2. 侧栏收起 / 主区让位:两者必须同一节奏,否则会一先一后地错开 */
|
|
1819
|
+
.app-nav { transition: width .34s var(--ease), box-shadow .3s ease; }
|
|
1820
|
+
main { transition: margin-left .34s var(--ease); }
|
|
1821
|
+
|
|
1822
|
+
/* 3. 分组折叠:高度收拢的同时内容同步上移淡出,盖住 max-height 的空转段 */
|
|
1823
|
+
.app-nav .tab-menu,
|
|
1824
|
+
.app-nav .tab-menu[hidden] {
|
|
1825
|
+
transition:
|
|
1826
|
+
max-height .36s var(--ease),
|
|
1827
|
+
opacity .24s ease,
|
|
1828
|
+
transform .36s var(--ease);
|
|
1829
|
+
transform-origin: top center;
|
|
1830
|
+
}
|
|
1831
|
+
@media (min-width: 721px) {
|
|
1832
|
+
.app-nav .tab-group.group-collapsed .tab-menu,
|
|
1833
|
+
.app-nav .tab-group.group-collapsed .tab-menu[hidden] {
|
|
1834
|
+
transform: translateY(-8px);
|
|
1835
|
+
/* 退场比入场快一档:用户已经决定收起,不必等动画慢慢同意 */
|
|
1836
|
+
transition-duration: .28s, .2s, .28s;
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
/* 4. 文字 / 图标 / 箭头:颜色不硬切,出现时带一点缩放而不是凭空跳出 */
|
|
1841
|
+
@keyframes nav-ico-in {
|
|
1842
|
+
from { opacity: 0; transform: scale(.92); }
|
|
1843
|
+
to { opacity: 1; transform: none; }
|
|
1844
|
+
}
|
|
1845
|
+
.app-nav .tab-parent { transition: color .18s ease, opacity .22s ease; }
|
|
1846
|
+
.app-nav .tab-item {
|
|
1847
|
+
transition: background-color .2s ease, color .2s ease,
|
|
1848
|
+
opacity .2s ease, box-shadow .2s ease;
|
|
1849
|
+
}
|
|
1850
|
+
.app-nav .tab-parent .nav-label {
|
|
1851
|
+
transition: max-width .34s var(--ease), opacity .26s ease;
|
|
1852
|
+
}
|
|
1853
|
+
.app-nav .tab-parent .nav-ico { transition: opacity .26s ease; }
|
|
1854
|
+
.app-nav .tab-group .tab-parent .chev {
|
|
1855
|
+
transition: transform .3s var(--ease), opacity .22s ease;
|
|
1856
|
+
}
|
|
1857
|
+
@media (min-width: 1024px) {
|
|
1858
|
+
body.nav-compact .app-nav .tab-parent .nav-ico {
|
|
1859
|
+
animation: nav-ico-in .3s var(--ease) both;
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
@media (min-width: 721px) and (max-width: 1023px) {
|
|
1863
|
+
.app-nav.expanded .tab-parent .chev {
|
|
1864
|
+
animation: nav-ico-in .3s var(--ease) both;
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
/* 5. 手机抽屉:容器淡入上滑,内容再分层入场,不再整块糊上来 */
|
|
1869
|
+
@keyframes drawer-in {
|
|
1870
|
+
from { opacity: .35; transform: translateY(22px); }
|
|
1871
|
+
to { opacity: 1; transform: none; }
|
|
1872
|
+
}
|
|
1873
|
+
@keyframes drawer-content-in {
|
|
1874
|
+
from { opacity: 0; transform: translateY(14px); }
|
|
1875
|
+
to { opacity: 1; transform: none; }
|
|
1876
|
+
}
|
|
1877
|
+
@media (max-width: 720px) {
|
|
1878
|
+
.app-nav.drawer-open {
|
|
1879
|
+
box-shadow: var(--shadow-lg);
|
|
1880
|
+
animation: drawer-in .36s cubic-bezier(.32, .72, 0, 1) both;
|
|
1881
|
+
}
|
|
1882
|
+
.app-nav.drawer-open .nav-head {
|
|
1883
|
+
animation: drawer-content-in .38s var(--ease) .04s both;
|
|
1884
|
+
}
|
|
1885
|
+
.app-nav.drawer-open .nav-scroll {
|
|
1886
|
+
animation: drawer-content-in .42s var(--ease) .08s both;
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
/* 6. 弹窗:入场位移看得见;手机改成从底部滑上来的 sheet */
|
|
1891
|
+
@keyframes modal-fade { from { opacity: 0 } to { opacity: 1 } }
|
|
1892
|
+
@keyframes modal-pop {
|
|
1893
|
+
from { opacity: 0; transform: translateY(14px) scale(.97); }
|
|
1894
|
+
to { opacity: 1; transform: none; }
|
|
1895
|
+
}
|
|
1896
|
+
@keyframes sheet-in {
|
|
1897
|
+
from { opacity: .7; transform: translateY(100%); }
|
|
1898
|
+
to { opacity: 1; transform: none; }
|
|
1899
|
+
}
|
|
1900
|
+
.modal-mask { animation: modal-fade .24s ease both; }
|
|
1901
|
+
.modal { animation: modal-pop .28s var(--ease) both; }
|
|
1902
|
+
@media (max-width: 720px) {
|
|
1903
|
+
.modal { animation: sheet-in .36s cubic-bezier(.32, .72, 0, 1) both; }
|
|
1904
|
+
}
|