Files
shanxia2.0/docs/fresh-mint-portal-home-demo.html
T
2026-08-09 14:51:41 +08:00

923 lines
29 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>