923 lines
29 KiB
HTML
923 lines
29 KiB
HTML
<!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>
|