网页通用模组 模块库 https://ai.njwl.ai/module/ · 可复用的网页能力组件 · 维护者 ben22

这些是网页里可以随时加上的通用模组,每个都给了「用途 + 代码 + 要点」,照着复制就能用。来源:cell-qr-app 的实战经验。

全页面水印

防泄密

内部页面铺一层旋转文字水印(公司 + 内部资料 + 时间),防截图外传。不挡点击。

/* CSS */
.page-watermark{position:fixed;inset:0;z-index:9999;pointer-events:none;display:grid;grid-template-columns:repeat(4,minmax(220px,1fr));grid-auto-rows:120px;opacity:.14}
.page-watermark span{color:#1f2329;font-weight:700;white-space:nowrap;transform:rotate(-24deg)}

/* JS */
const wm=document.createElement('div');wm.className='page-watermark';document.body.appendChild(wm);
function r(){wm.innerHTML=Array.from({length:36},()=>'<span>公司 · 内部资料 · '+new Date().toLocaleString('zh-CN',{hour12:false})+'</span>').join('');}
r();setInterval(r,60000);
要点pointer-events:none 别去掉,否则挡住点击;时间每 60 秒刷新;如需显示「谁在看」可接登录用户信息。

Toast 通知

操作反馈

保存 / 复制成功等操作后,屏幕中间弹一条提示,自动消失。

/* CSS */
.toast{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);padding:13px 20px;border-radius:10px;background:#087f5b;color:#fff;font-weight:800;opacity:0;transition:opacity .18s}
.toast.visible{opacity:1}
.toast.error{background:#c92a2a}

/* JS */
function toast(msg,err){const t=document.createElement('div');t.className='toast'+(err?' error':'');t.textContent=msg;document.body.appendChild(t);requestAnimationFrame(()=>t.classList.add('visible'));setTimeout(()=>t.remove(),1800);}
要点:成功绿 #087f5b、失败红 #c92a2arequestAnimationFrame 触发过渡。

复制按钮

一键复制

点一下复制文本(提示词 / 命令 / 链接),带剪贴板 API 和降级兜底。

async function copy(text){
  try{ await navigator.clipboard.writeText(text); }
  catch(e){
    const ta=document.createElement('textarea');ta.value=text;
    document.body.appendChild(ta);ta.select();document.execCommand('copy');ta.remove();
  }
  toast('已复制');
}
要点:HTTPS 下才能用 clipboard,非 HTTPS 自动走 execCommand 兜底。

浮动按钮

快捷操作

右下角圆形悬浮钮,做「回到顶部」「切换视图」等快捷入口。

/* CSS */
.fab{position:fixed;right:18px;bottom:18px;width:44px;height:44px;border-radius:50%;background:#3370ff;color:#fff;border:2px solid #fff;box-shadow:0 5px 18px rgba(36,91,219,.35);font-size:20px;cursor:pointer}
.fab:hover{background:#245bdb}

/* JS */
fab.addEventListener('click',()=>window.scrollTo({top:0,behavior:'smooth'}));
要点:白描边 + 蓝底 + 蓝投影,别让它挡住右下角重要内容。

分段 Tab

视图切换

一个页面里切换多个视图/分组,灰底 + 白激活块。

/* CSS */
.tabs{display:grid;grid-template-columns:repeat(4,1fr);gap:6px;padding:5px;border-radius:12px;background:#e9ebef}
.tab{border:0;border-radius:9px;background:transparent;color:#4e5969;font-weight:600;padding:9px 0;cursor:pointer}
.tab.active{background:#fff;color:#245bdb;box-shadow:0 1px 6px rgba(31,35,41,.1)}

/* JS:点 tab 切 active + 显示对应面板 */
tabs.forEach(t=>t.addEventListener('click',()=>{tabs.forEach(x=>x.classList.remove('active'));t.classList.add('active');showPanel(t.dataset.tab);}));
要点:激活项是「白块」不是「实心蓝」,这是这套设计的标志。

目录导航栏

侧边多级目录

长文档左侧多级目录:桌面端固定左侧、窄屏变成「fab 打开 + 遮罩关闭」的抽屉,目录从正文 h2/h3 自动生成。

/* CSS(移动端抽屉 + 桌面端固定) */
#sidebar{position:fixed;top:0;left:0;bottom:0;width:250px;background:#fff;border-right:1px solid #e2e6ed;z-index:100;overflow-y:auto;transform:translateX(-100%);transition:transform .22s ease}
body.sidebar-open #sidebar{transform:translateX(0)}
#scrim{display:none;position:fixed;inset:0;z-index:99;background:rgba(15,17,20,.4)}
body.sidebar-open #scrim{display:block}
#fab{display:flex;position:fixed;top:14px;left:14px;z-index:110;width:42px;height:42px;border-radius:9px;background:#3370ff;color:#fff;border:0;font-size:20px;cursor:pointer;align-items:center;justify-content:center}
.wrap{padding:24px 20px 60px}
@media (min-width:821px){#sidebar{transform:translateX(0)}.wrap{margin-left:250px}#fab{display:none}body.sidebar-open #scrim{display:none}}
/* HTML 结构 */
<button id="fab">☰</button>
<div id="scrim"></div>
<aside id="sidebar"><nav id="toc"></nav></aside>
<div class="wrap">…正文…</div>
/* JS:目录自动生成 + 开关 */
var nav = document.getElementById('toc');
// 从 .wrap 里的 h2/h3 生成多级目录(h2 一级、h3 缩进二级,见 deploy 页完整实现)

function setOpen(o){ document.body.classList.toggle('sidebar-open', o); }
document.getElementById('fab').addEventListener('click', function(){ setOpen(true); });
document.getElementById('scrim').addEventListener('click', function(){ setOpen(false); });
nav.addEventListener('click', function(e){ if (e.target.tagName === 'A') setOpen(false); });
要点:窄屏抽屉用 transform 平移 + 遮罩(scrim)关闭;桌面端用 @media (min-width) 固定左侧。开关用正向的 sidebar-open 状态。⚠️ 别在窄屏媒体查询里写 #sidebar{transform:translateX(-100%)} 强制隐藏——那样会盖过 open 状态,点 fab 没反应(常见 bug)。

折叠分组

内容分组

长表单/内容分组折叠,减少滚动,纯原生无需 JS。

/* HTML(原生 details) */
<details open>
  <summary>分组标题</summary>
  分组内容……
</details>

/* CSS */
details summary{list-style:none;cursor:pointer;font-weight:600}
details summary::-webkit-details-marker{display:none}
details summary::before{content:'▸ '}
details[open] summary::before{content:'▾ '}
要点:原生 <details> 免费拿到展开/收起,只需美化箭头。

高亮看板卡

数据概览

蓝色渐变高亮卡,突出核心数据 / 今日概览。

/* CSS */
.hero{background:linear-gradient(135deg,#245bdb,#3370ff);color:#fff;border-radius:14px;padding:18px;box-shadow:0 6px 18px rgba(36,91,219,.2)}
.hero .num{font-size:26px;line-height:1.1;font-weight:700}
要点:白字 + 大数字 + 半透明白底分隔,只用于整页最重要的一个概览。

文件预览

图片/PDF/音视频

点击缩略图或文件名 → 全屏深色预览:图片等比缩放、PDF/文本用 iframe、音视频用原生播放器,其它类型给下载兜底。原生 <dialog>,零依赖。

/* CSS */
.preview-dialog{width:min(94vw,1200px);max-width:none;height:min(92vh,900px);max-height:none;padding:44px 20px 16px;border:0;border-radius:14px;background:rgba(18,20,24,.96);color:#fff}
.preview-dialog::backdrop{background:rgba(15,17,20,.78);backdrop-filter:blur(3px)}
.preview-body{height:calc(100% - 36px);display:flex;align-items:center;justify-content:center;overflow:auto}
.preview-body img,.preview-body video{max-width:100%;max-height:100%}
.preview-body iframe{width:100%;height:100%;border:0;border-radius:6px;background:#fff}
.preview-caption{margin:9px 0 0;overflow:hidden;font-size:12px;text-align:center;text-overflow:ellipsis;white-space:nowrap}
.preview-close{position:absolute;top:9px;right:12px;width:32px;height:32px;padding:0;border-radius:50%;color:#fff;background:rgba(255,255,255,.16);font-size:24px;cursor:pointer}
/* JS:创建对话框,按扩展名分派渲染 */
function previewFile(url, name) {
  const ext = String(name || url || '').split('.').pop().toLowerCase();
  let dialog = document.querySelector('#universal-preview');
  if (!dialog) {
    dialog = document.createElement('dialog');
    dialog.id = 'universal-preview';
    dialog.className = 'preview-dialog';
    const close = document.createElement('button');
    close.type = 'button'; close.className = 'preview-close'; close.textContent = '×';
    close.setAttribute('aria-label', '关闭预览');
    const body = document.createElement('div'); body.className = 'preview-body';
    const caption = document.createElement('p'); caption.className = 'preview-caption';
    close.addEventListener('click', () => dialog.close());
    dialog.addEventListener('click', e => { if (e.target === dialog) dialog.close(); });
    dialog.append(close, body, caption);
    document.body.appendChild(dialog);
  }
  const body = dialog.querySelector('.preview-body');
  const caption = dialog.querySelector('.preview-caption');
  caption.textContent = name || url;
  body.replaceChildren();
  let el;
  if (/^(jpg|jpeg|png|gif|webp|svg|bmp|avif)$/.test(ext)) {
    el = document.createElement('img'); el.src = url; el.alt = name || '';
  } else if (ext === 'pdf' || /^(txt|md|json|log|csv|xml|html|htm)$/.test(ext)) {
    el = document.createElement('iframe'); el.src = url;
  } else if (/^(mp4|webm|mov|m4v)$/.test(ext)) {
    el = document.createElement('video'); el.src = url; el.controls = true;
  } else if (/^(mp3|wav|m4a|ogg|flac)$/.test(ext)) {
    el = document.createElement('audio'); el.src = url; el.controls = true;
  } else {
    const p = document.createElement('p'); p.textContent = '该类型暂不支持在线预览,请下载后查看。';
    const a = document.createElement('a'); a.href = url; a.textContent = '下载文件'; a.setAttribute('download', '');
    body.append(p, a); dialog.showModal(); return;
  }
  body.appendChild(el);
  dialog.showModal();
}

调用方法

// 在缩略图/文件列表的点击事件里调用,传「文件 URL + 文件名」
previewFile('https://example.com/产品手册.pdf', '产品手册.pdf');
previewFile('https://example.com/现场照片.jpg', '现场照片');
previewFile('https://example.com/演示视频.mp4', '演示视频');
要点:图片用 img(等比缩放);PDF 和文本用 iframe(依赖浏览器内置 PDF 查看器,Chrome / Edge / Firefox 都有);音视频用原生播放器;其它类型给「下载」兜底。iframe 预览要求文件 URL 可被浏览器直接访问(同域或允许跨域嵌入)。

访问心跳

需后端

记录「谁、什么时候、在看哪个页面、停留多久」,用于安全审计。

// 进入页面
fetch('api/heartbeat',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json'},body:JSON.stringify({page:location.pathname})});
// 离开/切走时上报(keepalive 保证发出)
document.addEventListener('visibilitychange',()=>{if(document.visibilityState==='hidden')fetch('api/heartbeat',{method:'POST',body:JSON.stringify({closed:true}),keepalive:true});});
要点:需要后端 /api/heartbeat 接口,纯静态页用不了。配合登录态能知道「谁在看」。