这些是网页里可以随时加上的通用模组,每个都给了「用途 + 代码 + 要点」,照着复制就能用。来源: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 秒刷新;如需显示「谁在看」可接登录用户信息。保存 / 复制成功等操作后,屏幕中间弹一条提示,自动消失。
/* 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、失败红 #c92a2a;requestAnimationFrame 触发过渡。点一下复制文本(提示词 / 命令 / 链接),带剪贴板 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('已复制');
}
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'}));
一个页面里切换多个视图/分组,灰底 + 白激活块。
/* 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/文本用 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 接口,纯静态页用不了。配合登录态能知道「谁在看」。