sidebar-group-leaf-tabs-demo.html 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1" />
  6. <title>侧栏目录 + 右侧叶子 Tab Demo</title>
  7. <link rel="preconnect" href="https://fonts.googleapis.com" />
  8. <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
  9. <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
  10. <style>
  11. :root {
  12. --primary: #10b981;
  13. --primary-dark: #059669;
  14. --primary-bg: #ecfdf5;
  15. --text: #1e293b;
  16. --text-muted: #64748b;
  17. --border: #d9e8e2;
  18. --bg-page: #f8fafc;
  19. --surface: #ffffff;
  20. --table-head: #f8fafc;
  21. --row-alt: #f1f5f9;
  22. --shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
  23. --radius: 10px;
  24. --sidebar-w: 220px;
  25. --header-h: 56px;
  26. }
  27. * { box-sizing: border-box; }
  28. html, body { height: 100%; }
  29. body {
  30. margin: 0;
  31. font-family: Inter, -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
  32. font-size: 14px;
  33. line-height: 1.5;
  34. color: var(--text);
  35. background: var(--bg-page);
  36. }
  37. button { font-family: inherit; }
  38. .page {
  39. max-width: 1280px;
  40. margin: 0 auto;
  41. padding: 20px 20px 48px;
  42. }
  43. .intro h1 {
  44. margin: 0 0 6px;
  45. font-size: 20px;
  46. font-weight: 700;
  47. }
  48. .intro p {
  49. margin: 0 0 12px;
  50. color: var(--text-muted);
  51. font-size: 13px;
  52. max-width: 860px;
  53. }
  54. .intro ul {
  55. margin: 0 0 16px;
  56. padding: 10px 14px 10px 28px;
  57. background: #fff;
  58. border: 1px solid var(--border);
  59. border-radius: 8px;
  60. font-size: 13px;
  61. color: var(--text-muted);
  62. }
  63. .intro li { margin: 3px 0; }
  64. .intro b { color: var(--text); font-weight: 600; }
  65. .shell {
  66. display: grid;
  67. grid-template-columns: var(--sidebar-w) 1fr;
  68. grid-template-rows: var(--header-h) minmax(560px, calc(100vh - 220px));
  69. border: 1px solid var(--border);
  70. border-radius: 12px;
  71. overflow: hidden;
  72. background: var(--surface);
  73. box-shadow: var(--shadow);
  74. }
  75. .header {
  76. grid-column: 1 / -1;
  77. display: flex;
  78. align-items: center;
  79. gap: 12px;
  80. height: var(--header-h);
  81. padding: 0 16px;
  82. border-bottom: 1px solid var(--border);
  83. background: #fff;
  84. }
  85. .brand {
  86. width: var(--sidebar-w);
  87. margin-left: -16px;
  88. padding-left: 16px;
  89. font-weight: 700;
  90. color: var(--primary-dark);
  91. }
  92. .crumb {
  93. color: var(--text-muted);
  94. font-size: 13px;
  95. }
  96. .crumb strong { color: var(--text); font-weight: 600; }
  97. .sidebar {
  98. border-right: 1px solid var(--border);
  99. background: #fff;
  100. overflow: auto;
  101. padding: 10px 8px 16px;
  102. }
  103. .side-label {
  104. padding: 6px 10px 8px;
  105. font-size: 11px;
  106. font-weight: 600;
  107. letter-spacing: 0.04em;
  108. color: var(--text-muted);
  109. text-transform: uppercase;
  110. }
  111. .tree-item { margin: 2px 0; }
  112. .tree-btn {
  113. width: 100%;
  114. display: flex;
  115. align-items: center;
  116. gap: 6px;
  117. border: 0;
  118. background: transparent;
  119. color: var(--text);
  120. text-align: left;
  121. padding: 8px 10px;
  122. border-radius: 8px;
  123. cursor: pointer;
  124. font-size: 14px;
  125. }
  126. .tree-btn:hover { background: var(--row-alt); }
  127. .tree-btn.is-active {
  128. background: var(--primary-bg);
  129. color: var(--primary-dark);
  130. font-weight: 600;
  131. }
  132. .tree-btn .chev {
  133. width: 14px;
  134. color: var(--text-muted);
  135. font-size: 11px;
  136. flex: 0 0 14px;
  137. }
  138. .tree-btn.is-active .chev { color: var(--primary-dark); }
  139. .tree-btn.l1 { font-weight: 600; }
  140. .tree-children {
  141. padding-left: 18px;
  142. display: none;
  143. }
  144. .tree-children.open { display: block; }
  145. .main {
  146. display: flex;
  147. flex-direction: column;
  148. min-width: 0;
  149. background: var(--bg-page);
  150. }
  151. .tabs {
  152. display: flex;
  153. padding: 12px 16px 8px;
  154. overflow-x: auto;
  155. }
  156. .tabs-track {
  157. display: inline-flex;
  158. flex-wrap: wrap;
  159. gap: 4px;
  160. padding: 4px;
  161. background: #fff;
  162. border: 1px solid var(--border);
  163. border-radius: 10px;
  164. box-shadow: 0 1px 2px rgba(30, 41, 59, 0.04), 0 8px 24px rgba(30, 41, 59, 0.04);
  165. }
  166. .tab {
  167. appearance: none;
  168. display: inline-flex;
  169. align-items: center;
  170. height: 32px;
  171. padding: 0 12px;
  172. font-size: 13px;
  173. font-weight: 500;
  174. color: #4b5563;
  175. cursor: pointer;
  176. white-space: nowrap;
  177. background: transparent;
  178. border: 0;
  179. border-radius: 8px;
  180. }
  181. .tab:hover { color: var(--text); background: #f3f6f4; }
  182. .tab.is-on {
  183. color: var(--primary-dark);
  184. font-weight: 600;
  185. background: var(--primary-bg, #ecfdf5);
  186. }
  187. .tab .dot {
  188. display: inline-block;
  189. width: 6px;
  190. height: 6px;
  191. border-radius: 99px;
  192. background: #cbd5e1;
  193. margin-right: 6px;
  194. vertical-align: middle;
  195. }
  196. .tab.is-on .dot { background: var(--primary); }
  197. .tab.is-loaded .dot { background: #94a3b8; }
  198. .tab.is-on.is-loaded .dot { background: var(--primary); }
  199. .panel-wrap {
  200. flex: 1;
  201. position: relative;
  202. overflow: auto;
  203. padding: 16px;
  204. }
  205. .empty {
  206. height: 100%;
  207. min-height: 360px;
  208. display: grid;
  209. place-items: center;
  210. color: var(--text-muted);
  211. font-size: 13px;
  212. border: 1px dashed var(--border);
  213. border-radius: var(--radius);
  214. background: #fff;
  215. }
  216. .view {
  217. display: none;
  218. background: #fff;
  219. border: 1px solid var(--border);
  220. border-radius: var(--radius);
  221. min-height: 360px;
  222. overflow: hidden;
  223. }
  224. .view.is-show { display: block; }
  225. .view-head {
  226. display: flex;
  227. align-items: center;
  228. justify-content: space-between;
  229. padding: 12px 16px;
  230. border-bottom: 1px solid var(--border);
  231. background: var(--table-head);
  232. }
  233. .view-head h2 {
  234. margin: 0;
  235. font-size: 15px;
  236. font-weight: 700;
  237. display: flex;
  238. align-items: center;
  239. gap: 8px;
  240. }
  241. .view-head h2::before {
  242. content: "";
  243. width: 3px;
  244. height: 14px;
  245. border-radius: 2px;
  246. background: var(--primary);
  247. }
  248. .badge {
  249. font-size: 11px;
  250. font-weight: 600;
  251. color: var(--primary-dark);
  252. background: var(--primary-bg);
  253. border-radius: 999px;
  254. padding: 2px 8px;
  255. }
  256. .view-body { padding: 16px; }
  257. table {
  258. width: 100%;
  259. border-collapse: collapse;
  260. font-size: 13px;
  261. }
  262. th, td {
  263. padding: 10px 12px;
  264. text-align: left;
  265. border-bottom: 1px solid var(--border);
  266. }
  267. th {
  268. color: var(--text-muted);
  269. font-weight: 600;
  270. background: var(--table-head);
  271. }
  272. tbody tr:nth-child(even) { background: #fafefe; }
  273. .tag {
  274. display: inline-block;
  275. padding: 2px 8px;
  276. border-radius: 4px;
  277. font-size: 12px;
  278. font-weight: 600;
  279. color: #fff;
  280. background: #059669;
  281. }
  282. .tag.warn { background: #d97706; }
  283. .tag.info {
  284. background: transparent;
  285. color: var(--text);
  286. border: 1px solid var(--border);
  287. }
  288. .kv {
  289. display: grid;
  290. grid-template-columns: 140px 1fr;
  291. gap: 8px 16px;
  292. font-size: 13px;
  293. }
  294. .kv dt { color: var(--text-muted); }
  295. .kv dd { margin: 0; }
  296. .mask {
  297. display: none;
  298. position: absolute;
  299. inset: 16px;
  300. border-radius: var(--radius);
  301. background: rgba(255, 255, 255, 0.72);
  302. align-items: center;
  303. justify-content: center;
  304. color: var(--text-muted);
  305. font-size: 13px;
  306. font-weight: 600;
  307. }
  308. .mask.show { display: flex; }
  309. .spin {
  310. width: 18px;
  311. height: 18px;
  312. border: 2px solid #cbd5e1;
  313. border-top-color: var(--primary-dark);
  314. border-radius: 50%;
  315. margin-right: 8px;
  316. animation: rot 0.8s linear infinite;
  317. }
  318. @keyframes rot { to { transform: rotate(360deg); } }
  319. .log {
  320. margin-top: 14px;
  321. background: #fff;
  322. border: 1px solid var(--border);
  323. border-radius: 8px;
  324. padding: 10px 12px;
  325. font-size: 12px;
  326. color: var(--text-muted);
  327. max-height: 120px;
  328. overflow: auto;
  329. }
  330. .log b { color: var(--text); }
  331. </style>
  332. </head>
  333. <body>
  334. <div class="page">
  335. <div class="intro">
  336. <h1>侧栏非叶子目录 + 右侧叶子 Tab</h1>
  337. <p>
  338. 交互演示(非真实业务页):左侧只展示目录(非叶子)。点击<strong>最后一层非叶子</strong>后,右侧用 Tab 打开其全部叶子菜单。
  339. 切换目录会清空上一组 Tab。默认加载第一个 Tab;再点哪个 Tab 才加载哪个界面。
  340. </p>
  341. <ul>
  342. <li>试一下:点 <b>报销管理 → 基础配置</b>,再点 <b>备用金</b>,观察右侧 Tab 整组替换。</li>
  343. <li>Tab 上的灰点表示尚未加载;点过一次后视为已加载(本 Demo 做缓存,切回不再重复请求)。</li>
  344. </ul>
  345. </div>
  346. <div class="shell">
  347. <header class="header">
  348. <div class="brand">山峡化工</div>
  349. <div class="crumb" id="crumb">请选择左侧最后一层目录</div>
  350. </header>
  351. <aside class="sidebar" id="sidebar"></aside>
  352. <section class="main">
  353. <div class="tabs" id="tabs"></div>
  354. <div class="panel-wrap">
  355. <div class="empty" id="empty">点击左侧「基础配置 / 备用金 / 报销 / 视图中心」打开叶子菜单 Tab</div>
  356. <div id="views"></div>
  357. <div class="mask" id="mask"><span class="spin"></span><span id="maskText">正在加载界面…</span></div>
  358. </div>
  359. </section>
  360. </div>
  361. <div class="log" id="log"><b>加载日志</b>:尚未操作</div>
  362. </div>
  363. <script>
  364. const MENU = [
  365. {
  366. id: "bx",
  367. title: "报销管理",
  368. children: [
  369. {
  370. id: "bx-config",
  371. title: "基础配置",
  372. leaves: [
  373. { id: "expense-item", title: "费用项目管理", kind: "table",
  374. rows: [["差旅费", "费用", "启用"], ["招待费", "费用", "启用"], ["运费", "费用", "停用"]] },
  375. { id: "wf-advance", title: "工作流配置(备用金)", kind: "kv",
  376. fields: [["流程名称", "备用金审批流程"], ["状态", "启用"], ["节点", "部门负责人 → 财务"]] },
  377. { id: "wf-reimburse", title: "工作流配置(报销单)", kind: "kv",
  378. fields: [["流程名称", "报销单审批流程"], ["状态", "启用"], ["节点", "直属上级 → 财务 → 出纳"]] }
  379. ]
  380. },
  381. {
  382. id: "bx-advance",
  383. title: "备用金",
  384. leaves: [
  385. { id: "adv-apply", title: "备用金申请", kind: "table",
  386. rows: [["BYJ20260819002", "汪璇", "50000.00", "已通过"], ["BYJ20260819001", "汪璇", "50768.30", "已驳回"]] },
  387. { id: "adv-approve", title: "备用金审批", kind: "table",
  388. rows: [["BYJ20260722001", "汪璇", "待审批", "部门负责人"], ["BYJ20260806001", "汪璇", "待审批", "财务"]] },
  389. { id: "adv-issue", title: "备用金发放", kind: "table",
  390. rows: [["BYJ20260522002", "汪璇", "50000.00", "已发放"], ["BYJ20260703001", "汪璇", "20000.00", "已发放"]] }
  391. ]
  392. },
  393. {
  394. id: "bx-reimburse",
  395. title: "报销",
  396. leaves: [
  397. { id: "rb-apply", title: "报销申请", kind: "table",
  398. rows: [["BX20260901001", "张三", "1280.00", "待提交"], ["BX20260902002", "李四", "3560.50", "审批中"]] },
  399. { id: "rb-approve", title: "报销审批", kind: "table",
  400. rows: [["BX20260902002", "李四", "3560.50", "待审批"]] },
  401. { id: "rb-pay", title: "支付发放冲抵", kind: "table",
  402. rows: [["BX20260815001", "王五", "890.00", "待发放"]] },
  403. { id: "inv-admin", title: "发票夹管理", kind: "kv",
  404. fields: [["待核验", "12 张"], ["已入账", "86 张"], ["异常", "2 张"]] },
  405. { id: "inv-mine", title: "我的发票夹", kind: "kv",
  406. fields: [["本月上传", "4 张"], ["可报销", "3 张"], ["已占用", "1 张"]] }
  407. ]
  408. },
  409. {
  410. id: "bx-view",
  411. title: "视图中心",
  412. leaves: [
  413. { id: "view-mine", title: "我的视图", kind: "kv",
  414. fields: [["默认视图", "本月报销汇总"], ["自定义", "2 个"]] },
  415. { id: "view-team", title: "部门视图", kind: "kv",
  416. fields: [["可见范围", "本部门及下级"], ["刷新", "每日 08:00"]] }
  417. ]
  418. }
  419. ]
  420. }
  421. ]
  422. const sidebar = document.getElementById("sidebar")
  423. const tabsEl = document.getElementById("tabs")
  424. const viewsEl = document.getElementById("views")
  425. const emptyEl = document.getElementById("empty")
  426. const crumbEl = document.getElementById("crumb")
  427. const maskEl = document.getElementById("mask")
  428. const maskText = document.getElementById("maskText")
  429. const logEl = document.getElementById("log")
  430. let openL1 = "bx"
  431. let activeGroupId = null
  432. let activeLeafId = null
  433. let loaded = new Set()
  434. let loadTimer = null
  435. const logs = []
  436. function log(msg) {
  437. const t = new Date().toLocaleTimeString("zh-CN", { hour12: false })
  438. logs.unshift(t + " " + msg)
  439. logEl.innerHTML = "<b>加载日志</b><br>" + logs.slice(0, 8).join("<br>")
  440. }
  441. function findGroup(id) {
  442. for (const l1 of MENU) {
  443. const g = l1.children.find((c) => c.id === id)
  444. if (g) return { l1, g }
  445. }
  446. return null
  447. }
  448. function renderSidebar() {
  449. sidebar.innerHTML = '<div class="side-label">业务目录</div>'
  450. MENU.forEach((l1) => {
  451. const wrap = document.createElement("div")
  452. wrap.className = "tree-item"
  453. wrap.innerHTML =
  454. '<button class="tree-btn l1" data-l1="' + l1.id + '">' +
  455. '<span class="chev">' + (openL1 === l1.id ? "▼" : "▶") + "</span>" +
  456. l1.title + "</button>"
  457. const kids = document.createElement("div")
  458. kids.className = "tree-children" + (openL1 === l1.id ? " open" : "")
  459. l1.children.forEach((g) => {
  460. const b = document.createElement("button")
  461. b.className = "tree-btn" + (g.id === activeGroupId ? " is-active" : "")
  462. b.innerHTML = '<span class="chev"></span>' + g.title
  463. b.addEventListener("click", () => openGroup(g.id))
  464. kids.appendChild(b)
  465. })
  466. wrap.querySelector("[data-l1]").addEventListener("click", () => {
  467. openL1 = openL1 === l1.id ? "" : l1.id
  468. renderSidebar()
  469. })
  470. wrap.appendChild(kids)
  471. sidebar.appendChild(wrap)
  472. })
  473. }
  474. function openGroup(groupId) {
  475. if (activeGroupId === groupId) return
  476. const found = findGroup(groupId)
  477. if (!found) return
  478. loaded = new Set()
  479. activeGroupId = groupId
  480. activeLeafId = found.g.leaves[0].id
  481. crumbEl.innerHTML =
  482. "<strong>" + found.l1.title + "</strong> / " + found.g.title +
  483. " · 叶子 " + found.g.leaves.length + " 个"
  484. emptyEl.style.display = "none"
  485. renderSidebar()
  486. renderTabs(found.g)
  487. renderViews(found.g)
  488. log("切换目录「" + found.g.title + "」,清空上一组 Tab,准备打开 " +
  489. found.g.leaves.map((x) => x.title).join(" / "))
  490. loadLeaf(found.g.leaves[0], true)
  491. }
  492. function renderTabs(group) {
  493. tabsEl.innerHTML = ""
  494. if (!group || group.leaves.length <= 1) return
  495. const track = document.createElement("div")
  496. track.className = "tabs-track"
  497. group.leaves.forEach((leaf) => {
  498. const b = document.createElement("button")
  499. b.className = "tab" +
  500. (leaf.id === activeLeafId ? " is-on" : "") +
  501. (loaded.has(leaf.id) ? " is-loaded" : "")
  502. b.innerHTML = '<span class="dot"></span>' + leaf.title
  503. b.addEventListener("click", () => selectLeaf(leaf.id))
  504. track.appendChild(b)
  505. })
  506. tabsEl.appendChild(track)
  507. }
  508. function tableHtml(leaf) {
  509. const isStatus = leaf.rows[0] && leaf.rows[0].length === 4
  510. const heads = isStatus
  511. ? (leaf.id.indexOf("adv") === 0 || leaf.id.indexOf("rb") === 0
  512. ? ["单号", "申请人", leaf.rows[0][2].indexOf(".") >= 0 ? "金额" : "状态", "状态/节点"]
  513. : ["名称", "类型", "状态"])
  514. : ["列1", "列2", "列3"]
  515. if (leaf.id === "expense-item") {
  516. return headTable(["项目名称", "类型", "状态"], leaf.rows, (c, i) =>
  517. i === 2 ? tagHtml(c) : escapeHtml(c))
  518. }
  519. if (leaf.rows[0].length === 4) {
  520. return headTable(
  521. leaf.id === "adv-approve"
  522. ? ["单号", "申请人", "状态", "当前节点"]
  523. : ["单号", "申请人", "金额", "状态"],
  524. leaf.rows,
  525. (c, i) => i === 3 || (leaf.id === "adv-approve" && i === 2) ? tagHtml(c) : escapeHtml(c)
  526. )
  527. }
  528. return headTable(heads, leaf.rows, (c) => escapeHtml(c))
  529. }
  530. function headTable(heads, rows, cellFn) {
  531. let h = "<table><thead><tr>" + heads.map((x) => "<th>" + x + "</th>").join("") + "</tr></thead><tbody>"
  532. rows.forEach((r) => {
  533. h += "<tr>" + r.map((c, i) => "<td>" + cellFn(c, i) + "</td>").join("") + "</tr>"
  534. })
  535. return h + "</tbody></table>"
  536. }
  537. function tagHtml(text) {
  538. const t = String(text)
  539. let cls = "tag"
  540. if (t.indexOf("驳回") >= 0 || t.indexOf("停用") >= 0) cls += " warn"
  541. if (t.indexOf("待") >= 0) cls += " info"
  542. return '<span class="' + cls + '">' + escapeHtml(t) + "</span>"
  543. }
  544. function kvHtml(leaf) {
  545. return '<dl class="kv">' + leaf.fields.map(function (f) {
  546. return "<dt>" + escapeHtml(f[0]) + "</dt><dd>" + escapeHtml(f[1]) + "</dd>"
  547. }).join("") + "</dl>"
  548. }
  549. function escapeHtml(s) {
  550. return String(s).replace(/[&<>]/g, function (ch) {
  551. return ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[ch]
  552. })
  553. }
  554. function renderViews(group) {
  555. viewsEl.innerHTML = ""
  556. group.leaves.forEach((leaf) => {
  557. const el = document.createElement("div")
  558. el.className = "view" + (leaf.id === activeLeafId ? " is-show" : "")
  559. el.id = "view-" + leaf.id
  560. const body = loaded.has(leaf.id)
  561. ? (leaf.kind === "kv" ? kvHtml(leaf) : tableHtml(leaf))
  562. : '<p style="color:#64748b;margin:0">尚未加载。切换到该 Tab 后才会请求并渲染本界面。</p>'
  563. el.innerHTML =
  564. '<div class="view-head"><h2>' + escapeHtml(leaf.title) +
  565. '</h2><span class="badge">' + (loaded.has(leaf.id) ? "已加载" : "未加载") +
  566. "</span></div><div class=\"view-body\">" + body + "</div>"
  567. viewsEl.appendChild(el)
  568. })
  569. }
  570. function selectLeaf(leafId) {
  571. const found = findGroup(activeGroupId)
  572. if (!found) return
  573. const leaf = found.g.leaves.find((x) => x.id === leafId)
  574. if (!leaf) return
  575. activeLeafId = leafId
  576. renderTabs(found.g)
  577. Array.from(viewsEl.children).forEach((el) => {
  578. el.classList.toggle("is-show", el.id === "view-" + leafId)
  579. })
  580. if (loaded.has(leafId)) {
  581. log("切换到「" + leaf.title + "」(已缓存,不再请求)")
  582. return
  583. }
  584. loadLeaf(leaf, false)
  585. }
  586. function loadLeaf(leaf, isDefault) {
  587. if (loadTimer) clearTimeout(loadTimer)
  588. maskText.textContent = "正在加载「" + leaf.title + "」…"
  589. maskEl.classList.add("show")
  590. log((isDefault ? "默认打开第一个 Tab:" : "切换 Tab,按需加载:") + "「" + leaf.title + "」")
  591. loadTimer = setTimeout(function () {
  592. loaded.add(leaf.id)
  593. maskEl.classList.remove("show")
  594. const found = findGroup(activeGroupId)
  595. renderTabs(found.g)
  596. renderViews(found.g)
  597. log("「" + leaf.title + "」加载完成")
  598. }, 420)
  599. }
  600. renderSidebar()
  601. </script>
  602. </body>
  603. </html>