| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922 |
- <!doctype html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <title>Fresh Mint · 首页菜单目录 Demo(找得到)</title>
- <link rel="preconnect" href="https://fonts.googleapis.com" />
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
- <style>
- :root {
- --primary: #10b981;
- --primary-dark: #059669;
- --primary-bg: #ecfdf5;
- --text: #1e293b;
- --text-muted: #64748b;
- --border: #d9e8e2;
- --surface: #ffffff;
- --bg-page: #f8fafc;
- --shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
- --radius: 10px;
- --warn: #d97706;
- --row-hover: #ecfdf5;
- --hit: #065f46;
- }
- * { box-sizing: border-box; }
- body {
- margin: 0;
- min-height: 100vh;
- font-family: Inter, -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
- font-size: 14px;
- line-height: 1.5;
- color: var(--text);
- background: var(--bg-page);
- }
- button, input { font-family: inherit; }
- .page { max-width: 1080px; margin: 0 auto; padding: 24px 16px 72px; }
- .intro { margin-bottom: 16px; }
- .intro h1 {
- margin: 0 0 6px;
- font-size: 20px;
- font-weight: 700;
- }
- .intro h1 span { color: var(--primary); }
- .intro p {
- margin: 0;
- font-size: 13px;
- color: var(--text-muted);
- max-width: 760px;
- }
- .intro ul {
- margin: 10px 0 0;
- padding: 10px 12px 10px 28px;
- background: #fff;
- border: 1px solid var(--border);
- border-radius: 8px;
- font-size: 12px;
- color: var(--text-muted);
- }
- .intro li { margin: 3px 0; }
- .intro b { color: var(--text); font-weight: 600; }
- /* 工作区 */
- .shell {
- border: 1px solid var(--border);
- border-radius: 12px;
- background: #fff;
- box-shadow: var(--shadow);
- overflow: hidden;
- display: grid;
- grid-template-rows: auto 1fr;
- min-height: 620px;
- max-height: calc(100vh - 140px);
- }
- .search-bar {
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 14px 16px;
- border-bottom: 1px solid var(--border);
- background: linear-gradient(180deg, #ecfdf5 0%, #fff 85%);
- }
- .search-wrap {
- position: relative;
- flex: 1;
- }
- .search-wrap svg {
- position: absolute;
- left: 12px;
- top: 50%;
- transform: translateY(-50%);
- width: 18px;
- height: 18px;
- color: var(--text-muted);
- pointer-events: none;
- }
- .search-wrap input {
- width: 100%;
- height: 42px;
- padding: 0 96px 0 40px;
- border: 1px solid var(--border);
- border-radius: 8px;
- font-size: 15px;
- font-weight: 500;
- outline: none;
- background: #fff;
- }
- .search-wrap input:focus {
- border-color: var(--primary);
- box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.15);
- }
- .search-wrap .hint {
- position: absolute;
- right: 10px;
- top: 50%;
- transform: translateY(-50%);
- font-size: 11px;
- color: #94a3b8;
- pointer-events: none;
- }
- .search-meta {
- flex-shrink: 0;
- font-size: 12px;
- color: var(--text-muted);
- white-space: nowrap;
- }
- .search-meta b {
- color: var(--primary-dark);
- font-variant-numeric: tabular-nums;
- }
- .body {
- display: grid;
- grid-template-columns: 168px 1fr 36px;
- min-height: 0;
- }
- @media (max-width: 800px) {
- .body { grid-template-columns: 1fr; }
- .index, .letters { display: none !important; }
- }
- /* 左侧:模块索引(只负责跳转/过滤,不铺卡片) */
- .index {
- border-right: 1px solid var(--border);
- background: #f8fafc;
- overflow: auto;
- padding: 8px 6px;
- }
- .index-item {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 6px;
- width: 100%;
- padding: 8px 10px;
- margin: 1px 0;
- border: 0;
- border-radius: 8px;
- background: transparent;
- color: var(--text);
- font-size: 13px;
- font-weight: 500;
- text-align: left;
- cursor: pointer;
- }
- .index-item:hover { background: #eef2f7; }
- .index-item.is-on {
- background: var(--primary-bg);
- color: var(--primary-dark);
- font-weight: 700;
- }
- .index-item .n {
- font-size: 11px;
- font-weight: 600;
- color: #94a3b8;
- font-variant-numeric: tabular-nums;
- }
- .index-item.is-on .n { color: var(--primary-dark); }
- /* 中间:可扫读的菜单目录 */
- .dir {
- overflow: auto;
- min-height: 0;
- padding: 0 0 16px;
- scroll-behavior: smooth;
- }
- .fav-box {
- margin: 12px 14px 8px;
- padding: 10px 12px;
- border: 1px solid #bbf7d0;
- border-radius: 8px;
- background: #f0fdf4;
- }
- .fav-box.is-hidden { display: none; }
- .fav-box__title {
- margin: 0 0 8px;
- font-size: 12px;
- font-weight: 700;
- color: var(--primary-dark);
- }
- .fav-links {
- display: flex;
- flex-wrap: wrap;
- gap: 6px 14px;
- }
- .fav-links a {
- font-size: 13px;
- font-weight: 600;
- color: var(--text);
- text-decoration: none;
- border-bottom: 1px dashed transparent;
- }
- .fav-links a:hover {
- color: var(--primary-dark);
- border-bottom-color: var(--primary);
- }
- .group {
- padding: 4px 0 2px;
- scroll-margin-top: 8px;
- }
- .group.is-hidden { display: none; }
- .group__head {
- position: sticky;
- top: 0;
- z-index: 2;
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 8px 14px;
- background: rgba(255, 255, 255, 0.96);
- border-bottom: 1px solid #eef2f0;
- backdrop-filter: none;
- }
- .group__head h3 {
- margin: 0;
- font-size: 13px;
- font-weight: 700;
- }
- .group__head span {
- font-size: 11px;
- color: var(--text-muted);
- font-weight: 500;
- }
- /* 关键:多列文字链接,一眼扫名称 */
- .menu-cols {
- display: grid;
- grid-template-columns: repeat(3, minmax(0, 1fr));
- gap: 0 8px;
- padding: 4px 10px 10px;
- }
- @media (max-width: 960px) {
- .menu-cols { grid-template-columns: repeat(2, minmax(0, 1fr)); }
- }
- @media (max-width: 560px) {
- .menu-cols { grid-template-columns: 1fr; }
- }
- .menu-row {
- display: grid;
- grid-template-columns: 1fr auto;
- align-items: center;
- gap: 4px;
- min-height: 34px;
- padding: 4px 8px;
- border-radius: 6px;
- cursor: pointer;
- border: 0;
- background: transparent;
- text-align: left;
- width: 100%;
- color: inherit;
- }
- .menu-row:hover,
- .menu-row.is-active {
- background: var(--row-hover);
- }
- .menu-row.is-hidden { display: none; }
- .menu-row__name {
- font-size: 13px;
- font-weight: 600;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- .menu-row__name mark {
- background: #a7f3d0;
- color: var(--hit);
- border-radius: 2px;
- padding: 0 1px;
- font-weight: 700;
- }
- .menu-row__path {
- display: none;
- font-size: 11px;
- color: var(--text-muted);
- margin-top: 1px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- /* 搜索时显示路径,帮助消歧 */
- .dir.is-filtering .menu-row__path { display: block; }
- .dir.is-filtering .menu-cols {
- grid-template-columns: 1fr;
- gap: 0;
- padding: 4px 10px 12px;
- }
- .dir.is-filtering .menu-row {
- grid-template-columns: 1fr 28px;
- min-height: 40px;
- padding: 6px 10px;
- border-bottom: 1px solid #f1f5f9;
- border-radius: 0;
- }
- .menu-row__star {
- width: 24px;
- height: 24px;
- border: 0;
- border-radius: 6px;
- background: transparent;
- color: #cbd5e1;
- cursor: pointer;
- display: grid;
- place-items: center;
- padding: 0;
- opacity: 0;
- flex-shrink: 0;
- }
- .menu-row:hover .menu-row__star,
- .menu-row__star.is-on { opacity: 1; }
- .menu-row__star.is-on { color: var(--warn); }
- .menu-row__star:hover { background: #fff7ed; color: var(--warn); }
- .empty {
- display: none;
- padding: 48px 20px;
- text-align: center;
- color: var(--text-muted);
- font-size: 13px;
- }
- .empty.show { display: block; }
- .empty code {
- padding: 1px 6px;
- border-radius: 4px;
- background: #f1f5f9;
- color: var(--text);
- }
- /* 右侧:拼音首字母快速跳转 */
- .letters {
- border-left: 1px solid var(--border);
- background: #f8fafc;
- display: flex;
- flex-direction: column;
- align-items: center;
- padding: 8px 0;
- gap: 1px;
- overflow: auto;
- }
- .letters button {
- width: 28px;
- height: 20px;
- border: 0;
- border-radius: 4px;
- background: transparent;
- color: var(--text-muted);
- font-size: 11px;
- font-weight: 700;
- cursor: pointer;
- padding: 0;
- }
- .letters button:hover:not(:disabled) {
- background: var(--primary-bg);
- color: var(--primary-dark);
- }
- .letters button:disabled {
- opacity: 0.25;
- cursor: default;
- }
- .letters button.is-on {
- background: var(--primary);
- color: #fff;
- }
- .toast {
- position: fixed;
- left: 50%;
- bottom: 24px;
- transform: translateX(-50%) translateY(12px);
- opacity: 0;
- padding: 8px 14px;
- border-radius: 8px;
- background: #1e293b;
- color: #fff;
- font-size: 13px;
- transition: .18s ease;
- pointer-events: none;
- z-index: 30;
- }
- .toast.show {
- opacity: 1;
- transform: translateX(-50%) translateY(0);
- }
- </style>
- </head>
- <body>
- <div class="page">
- <header class="intro">
- <h1>首页 · <span>菜单目录</span>(以「找得到」为目标)</h1>
- <p>不做卡片墙、不做整行模块区。首页就是一张可搜索的菜单目录:左边点模块,中间扫名称,右边按拼音首字母跳转;搜索即时过滤并高亮匹配字。</p>
- <ul>
- <li><b>记得名字</b> → 直接输入(支持中文 / 拼音 / 首字母,如 <code>ht</code>、<code>hetong</code>、<code>合同</code>)</li>
- <li><b>记得模块</b> → 左侧点模块,中间只看该模块</li>
- <li><b>按字母找</b> → 右侧 A–Z 跳到对应首字母分组</li>
- <li><b>常用</b> → 顶部收藏链接,一键直达</li>
- </ul>
- </header>
- <div class="shell">
- <div class="search-bar">
- <div class="search-wrap">
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
- <circle cx="11" cy="11" r="7" /><path d="m20 20-3.5-3.5" />
- </svg>
- <input id="q" type="search" placeholder="输入菜单名 / 拼音 / 首字母,回车打开第一条" autocomplete="off" autofocus />
- <span class="hint">Enter 打开 · Esc 清空</span>
- </div>
- <div class="search-meta">匹配 <b id="hit-n">0</b> / <span id="total-n">0</span></div>
- </div>
- <div class="body">
- <aside class="index" id="index" aria-label="模块"></aside>
- <main class="dir" id="dir">
- <div class="fav-box" id="fav-box">
- <div class="fav-box__title">常用</div>
- <div class="fav-links" id="fav-links"></div>
- </div>
- <div id="groups"></div>
- <div class="empty" id="empty">没有匹配 <code id="empty-q"></code> 的菜单。可换拼音首字母试试,如 <code>ht</code></div>
- </main>
- <aside class="letters" id="letters" aria-label="拼音首字母"></aside>
- </div>
- </div>
- </div>
- <div class="toast" id="toast"></div>
- <script>
- /**
- * 目标:最快找到菜单。
- * - 即时过滤(不切页面)
- * - 密排文字目录(比图标墙更好扫读)
- * - 拼音 / 首字母(演示用内置字段)
- * - 左侧模块 + 右侧字母双索引
- */
- const MODULES = [
- {
- id: 'crm', title: 'CRM 管理',
- items: [
- { name: '客户档案', py: 'kehu dang\'an', ab: 'khda' },
- { name: '联系人', py: 'lianxiren', ab: 'lxr' },
- { name: '商机跟进', py: 'shangji genjin', ab: 'sjgj' },
- { name: '合同管理', py: 'hetong guanli', ab: 'htgl' },
- { name: '回款记录', py: 'huikuan jilu', ab: 'hkjl' },
- { name: '报价单', py: 'baojia dan', ab: 'bjd' },
- { name: '拜访计划', py: 'baifang jihua', ab: 'bfjh' },
- { name: '公海池', py: 'gonghai chi', ab: 'ghc' }
- ]
- },
- {
- id: 'pl', title: '计划物流',
- items: [
- { name: '运输计划', py: 'yunshu jihua', ab: 'ysjh' },
- { name: '车辆调度', py: 'cheliang diaodu', ab: 'cldd' },
- { name: '人员排班', py: 'renyuan paiban', ab: 'rypb' },
- { name: '运力看板', py: 'yunli kanban', ab: 'ylkb' },
- { name: '线路管理', py: 'xianlu guanli', ab: 'xlgl' },
- { name: '运费结算', py: 'yunfei jiesuan', ab: 'yfjs' },
- { name: '异常反馈', py: 'yichang fankui', ab: 'ycfk' }
- ]
- },
- {
- id: 'delivery', title: '配送业务',
- items: [
- { name: '配送订单', py: 'peisong dingdan', ab: 'psdd' },
- { name: '配送任务', py: 'peisong renwu', ab: 'psrw' },
- { name: '签收管理', py: 'qianshou guanli', ab: 'qsgl' },
- { name: '配送员', py: 'peisong yuan', ab: 'psy' },
- { name: '站点配置', py: 'zhandian peizhi', ab: 'zdpz' }
- ]
- },
- {
- id: 'weigh', title: '地磅协同',
- items: [
- { name: '过磅记录', py: 'guobang jilu', ab: 'gbjl' },
- { name: '车辆登记', py: 'cheliang dengji', ab: 'cldj' },
- { name: '物资对照', py: 'wuzi duizhao', ab: 'wzd z' },
- { name: '异常过磅', py: 'yichang guobang', ab: 'ycgb' }
- ]
- },
- {
- id: 'product', title: '商品中心',
- items: [
- { name: '商品列表', py: 'shangpin liebiao', ab: 'splb' },
- { name: '分类管理', py: 'fenlei guanli', ab: 'flgl' },
- { name: '计量单位', py: 'jiliang danwei', ab: 'jldw' },
- { name: '价格策略', py: 'jiage celue', ab: 'jgcl' },
- { name: '库存概览', py: 'kucun gailan', ab: 'kcgl' }
- ]
- },
- {
- id: 'order', title: '订单中心',
- items: [
- { name: '销售订单', py: 'xiaoshou dingdan', ab: 'xsdd' },
- { name: '采购订单', py: 'caigou dingdan', ab: 'cgdd' },
- { name: '退货单', py: 'tuihuo dan', ab: 'thd' },
- { name: '对账单', py: 'duizhang dan', ab: 'dzd' }
- ]
- },
- {
- id: 'supplier', title: '供应商',
- items: [
- { name: '供应商档案', py: 'gongyingshang dang\'an', ab: 'gysda' },
- { name: '资质审核', py: 'zizhi shenhe', ab: 'zzsh' },
- { name: '供货协议', py: 'gonghuo xieyi', ab: 'ghxy' },
- { name: '评价记录', py: 'pingjia jilu', ab: 'pjjl' }
- ]
- },
- {
- id: 'system', title: '系统管理',
- items: [
- { name: '用户管理', py: 'yonghu guanli', ab: 'yhgl' },
- { name: '角色权限', py: 'juese quanxian', ab: 'jsqx' },
- { name: '部门管理', py: 'bumen guanli', ab: 'bmgl' },
- { name: '租户管理', py: 'zuhu guanli', ab: 'zhgl' },
- { name: '菜单管理', py: 'caidan guanli', ab: 'cdgl' },
- { name: '字典配置', py: 'zidian peizhi', ab: 'zdpz' },
- { name: '操作日志', py: 'caozuo rizhi', ab: 'czrz' }
- ]
- },
- {
- id: 'demo', title: 'Demo',
- items: [
- { name: '标准列表', py: 'biaozhun liebiao', ab: 'bzlb' },
- { name: '复杂列表', py: 'fuza liebiao', ab: 'fzlb' },
- { name: '商品列表', py: 'shangpin liebiao', ab: 'splb' },
- { name: '树形主数据', py: 'shuxing zhushuju', ab: 'sxzsj' },
- { name: '系统用户', py: 'xitong yonghu', ab: 'xtyh' }
- ]
- }
- ]
- // 修正笔误
- MODULES.find((m) => m.id === 'weigh').items[2].ab = 'wzdz'
- const FLAT = MODULES.flatMap((m) =>
- m.items.map((it) => ({
- ...it,
- moduleId: m.id,
- moduleTitle: m.title,
- letter: (it.py.replace(/^[^a-z]*/i, '')[0] || '#').toUpperCase(),
- path: m.title
- }))
- )
- let pins = ['合同管理', '运输计划', '配送订单', '用户管理']
- let activeModule = 'all'
- let activeLetter = ''
- let activeName = ''
- const starSvg = (on) =>
- on
- ? '<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg>'
- : '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m12 17.27 6.18 3.73-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27z"/></svg>'
- function toast(msg) {
- const el = document.getElementById('toast')
- el.textContent = msg
- el.classList.add('show')
- clearTimeout(toast._t)
- toast._t = setTimeout(() => el.classList.remove('show'), 1100)
- }
- function normalize(s) {
- return String(s || '')
- .toLowerCase()
- .replace(/['\s]/g, '')
- }
- function matchItem(it, q) {
- if (!q) return true
- const nq = normalize(q)
- return (
- it.name.includes(q) ||
- normalize(it.py).includes(nq) ||
- normalize(it.ab).includes(nq) ||
- normalize(it.moduleTitle).includes(nq)
- )
- }
- function highlight(name, q) {
- if (!q || !name.includes(q)) return name
- return name.split(q).join('<mark>' + q + '</mark>')
- }
- function openMenu(name) {
- activeName = name
- document.querySelectorAll('.menu-row').forEach((el) => {
- el.classList.toggle('is-active', el.getAttribute('data-name') === name)
- })
- toast('打开「' + name + '」')
- }
- function togglePin(name) {
- if (pins.includes(name)) pins = pins.filter((x) => x !== name)
- else pins = [name, ...pins]
- renderFav()
- // 更新星标状态
- document.querySelectorAll('.menu-row__star').forEach((btn) => {
- const n = btn.getAttribute('data-star')
- const on = pins.includes(n)
- btn.classList.toggle('is-on', on)
- btn.innerHTML = starSvg(on)
- })
- toast(pins.includes(name) ? '已加入常用' : '已取消常用')
- }
- function renderFav() {
- const box = document.getElementById('fav-box')
- const root = document.getElementById('fav-links')
- const q = document.getElementById('q').value.trim()
- const list = pins.filter((n) => {
- const it = FLAT.find((x) => x.name === n)
- return it && matchItem(it, q) && (activeModule === 'all' || it.moduleId === activeModule)
- })
- box.classList.toggle('is-hidden', !list.length && !pins.length)
- if (!pins.length) {
- root.innerHTML = '<span style="font-size:12px;color:#94a3b8">点击菜单右侧星标加入常用</span>'
- return
- }
- if (!list.length) {
- box.classList.add('is-hidden')
- return
- }
- box.classList.remove('is-hidden')
- root.innerHTML = list
- .map((n) => '<a href="#" data-open="' + n + '">' + n + '</a>')
- .join('')
- root.querySelectorAll('[data-open]').forEach((a) => {
- a.addEventListener('click', (e) => {
- e.preventDefault()
- openMenu(a.getAttribute('data-open'))
- })
- })
- }
- function renderIndex(counts) {
- const root = document.getElementById('index')
- const allCount = Object.values(counts).reduce((a, b) => a + b, 0)
- const rows = [{ id: 'all', title: '全部菜单', n: allCount }].concat(
- MODULES.map((m) => ({ id: m.id, title: m.title, n: counts[m.id] || 0 }))
- )
- root.innerHTML = rows
- .map((r) => {
- if (r.id !== 'all' && r.n === 0 && document.getElementById('q').value.trim()) {
- return ''
- }
- return (
- '<button type="button" class="index-item' +
- (activeModule === r.id ? ' is-on' : '') +
- '" data-mod="' +
- r.id +
- '"><span>' +
- r.title +
- '</span><span class="n">' +
- r.n +
- '</span></button>'
- )
- })
- .join('')
- root.querySelectorAll('[data-mod]').forEach((btn) => {
- btn.addEventListener('click', () => {
- activeModule = btn.getAttribute('data-mod')
- activeLetter = ''
- applyFilter({ scrollMod: activeModule === 'all' ? null : activeModule })
- })
- })
- }
- function renderLetters(available) {
- const root = document.getElementById('letters')
- const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')
- root.innerHTML = alphabet
- .map((L) => {
- const ok = available.has(L)
- return (
- '<button type="button" data-letter="' +
- L +
- '"' +
- (ok ? '' : ' disabled') +
- (activeLetter === L ? ' class="is-on"' : '') +
- '>' +
- L +
- '</button>'
- )
- })
- .join('')
- root.querySelectorAll('button[data-letter]:not(:disabled)').forEach((btn) => {
- btn.addEventListener('click', () => {
- const L = btn.getAttribute('data-letter')
- activeLetter = activeLetter === L ? '' : L
- // 字母跳转:滚到该字母第一条
- applyFilter({ scrollLetter: activeLetter || null })
- })
- })
- }
- function applyFilter(opts) {
- opts = opts || {}
- const q = document.getElementById('q').value.trim()
- const dir = document.getElementById('dir')
- const groupsRoot = document.getElementById('groups')
- dir.classList.toggle('is-filtering', !!q)
- const counts = {}
- MODULES.forEach((m) => (counts[m.id] = 0))
- const letters = new Set()
- let hit = 0
- let firstHitName = null
- groupsRoot.innerHTML = MODULES.map((m) => {
- const rows = m.items
- .map((it) => {
- const full = { ...it, moduleId: m.id, moduleTitle: m.title, path: m.title, letter: (it.py[0] || '#').toUpperCase() }
- const okMatch = matchItem(full, q)
- const okMod = activeModule === 'all' || activeModule === m.id
- const okLetter = !activeLetter || full.letter === activeLetter
- const show = okMatch && okMod && okLetter
- if (okMatch && (activeModule === 'all' || activeModule === m.id)) {
- // 计数不受字母限制,便于看模块存量;字母只影响列表显示
- }
- if (okMatch && okMod) {
- counts[m.id]++
- letters.add(full.letter)
- }
- if (!show) {
- return (
- '<button type="button" class="menu-row is-hidden" data-name="' +
- it.name +
- '" data-letter="' +
- full.letter +
- '"></button>'
- )
- }
- hit++
- if (!firstHitName) firstHitName = it.name
- const on = pins.includes(it.name)
- return (
- '<div class="menu-row-wrap">' +
- '<button type="button" class="menu-row' +
- (activeName === it.name ? ' is-active' : '') +
- '" data-name="' +
- it.name +
- '" data-letter="' +
- full.letter +
- '">' +
- '<span>' +
- '<div class="menu-row__name">' +
- highlight(it.name, q) +
- '</div>' +
- '<div class="menu-row__path">' +
- m.title +
- ' / ' +
- it.name +
- '</div>' +
- '</span>' +
- '<button type="button" class="menu-row__star' +
- (on ? ' is-on' : '') +
- '" data-star="' +
- it.name +
- '" aria-label="常用" tabindex="-1">' +
- starSvg(on) +
- '</button>' +
- '</button>' +
- '</div>'
- )
- })
- .join('')
- const groupShow = counts[m.id] > 0 && (activeModule === 'all' || activeModule === m.id)
- // 若选了字母,组内可能全隐藏
- const visibleInLetter = m.items.some((it) => {
- const letter = (it.py[0] || '#').toUpperCase()
- return matchItem({ ...it, moduleTitle: m.title }, q) && (!activeLetter || letter === activeLetter)
- })
- if (!groupShow || (activeLetter && !visibleInLetter && activeModule !== 'all' && activeModule !== m.id)) {
- /* keep */
- }
- const showGroup =
- (activeModule === 'all' || activeModule === m.id) &&
- counts[m.id] > 0 &&
- (!activeLetter ||
- m.items.some((it) => {
- const letter = (it.py[0] || '#').toUpperCase()
- return matchItem({ ...it, moduleTitle: m.title }, q) && letter === activeLetter
- }))
- return (
- '<section class="group' +
- (showGroup ? '' : ' is-hidden') +
- '" id="mod-' +
- m.id +
- '" data-mod="' +
- m.id +
- '">' +
- '<div class="group__head"><h3>' +
- m.title +
- '</h3><span>' +
- counts[m.id] +
- ' 项</span></div>' +
- '<div class="menu-cols">' +
- rows +
- '</div>' +
- '</section>'
- )
- }).join('')
- // 字母过滤时隐藏不匹配行(上面已写入 is-hidden;再扫一遍修正)
- groupsRoot.querySelectorAll('.menu-row').forEach((row) => {
- if (!row.getAttribute('data-name')) {
- row.remove()
- return
- }
- const name = row.getAttribute('data-name')
- const it = FLAT.find((x) => x.name === name)
- if (!it) return
- const okMatch = matchItem(it, q)
- const okMod = activeModule === 'all' || activeModule === it.moduleId
- const okLetter = !activeLetter || it.letter === activeLetter
- const show = okMatch && okMod && okLetter
- row.classList.toggle('is-hidden', !show)
- // 重新统计 hit(上面 hit 在字母时可能偏大)
- })
- // 精确重算 hit
- hit = groupsRoot.querySelectorAll('.menu-row:not(.is-hidden)').length
- firstHitName =
- (groupsRoot.querySelector('.menu-row:not(.is-hidden)') || {}).getAttribute?.('data-name') || null
- document.getElementById('hit-n').textContent = String(hit)
- document.getElementById('total-n').textContent = String(FLAT.length)
- document.getElementById('empty').classList.toggle('show', hit === 0)
- document.getElementById('empty-q').textContent = q || activeLetter || '当前条件'
- renderIndex(counts)
- renderLetters(letters)
- renderFav()
- groupsRoot.querySelectorAll('.menu-row:not(.is-hidden)').forEach((row) => {
- row.addEventListener('click', (e) => {
- if (e.target.closest('[data-star]')) return
- openMenu(row.getAttribute('data-name'))
- })
- })
- groupsRoot.querySelectorAll('[data-star]').forEach((btn) => {
- btn.addEventListener('click', (e) => {
- e.stopPropagation()
- togglePin(btn.getAttribute('data-star'))
- })
- })
- if (opts.scrollMod) {
- const el = document.getElementById('mod-' + opts.scrollMod)
- if (el && !el.classList.contains('is-hidden')) el.scrollIntoView({ block: 'start' })
- }
- if (opts.scrollLetter) {
- const el = groupsRoot.querySelector(
- '.menu-row:not(.is-hidden)[data-letter="' + opts.scrollLetter + '"]'
- )
- if (el) el.scrollIntoView({ block: 'center' })
- }
- applyFilter._first = firstHitName
- }
- const qInput = document.getElementById('q')
- qInput.addEventListener('input', () => {
- activeLetter = ''
- applyFilter()
- })
- qInput.addEventListener('keydown', (e) => {
- if (e.key === 'Enter') {
- e.preventDefault()
- if (applyFilter._first) openMenu(applyFilter._first)
- }
- if (e.key === 'Escape') {
- qInput.value = ''
- activeLetter = ''
- applyFilter()
- }
- })
- document.addEventListener('keydown', (e) => {
- if (e.key === '/' && document.activeElement !== qInput) {
- e.preventDefault()
- qInput.focus()
- qInput.select()
- }
- })
- applyFilter()
- qInput.focus()
- </script>
- </body>
- </html>
|