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

通用组件采用固定版本源码包与业务适配,按下方开发规范维护分叉和特性回送。历史代码片段保留供参考,逐步组件化。

通用组件开发规范

跨项目复用 · 规范 1.0

固定版本下发 → 项目按需 fork → 特性回送 → 母组件评估 → 新版本发布 → 项目选择升级。

适用于表格、弹窗、上传、文件浏览、导航等组件。应用使用可执行组件与业务适配,不按长提示词重新实现控件。

下载规范 Markdown · 查看首个实施示例:表格组件

开发、分叉与发布规范

规范版本:1.0 · 更新:2026-09-12。适用于表格、弹窗、上传、文件浏览、导航等可复用组件。默认采用“固定版本下发 → 项目按需 fork → 特性回送 → 母组件评估 → 发布新版本 → 项目选择升级”。

这是新建及后续改造组件的开发规范,不代表 module 中所有历史代码片段已经完成组件化。表格组件是首个实施示例;各组件应明确标注自己的封装、工具与验证状态。

1. 复用可执行组件,而非重新解释规范

  • 有成熟实现时,从经过验证的应用源码抽取,以它的实际结构、样式和交互为基准。文档和截图用于说明,不作为重新实现一套相似控件的指令。
  • 母组件只有一份可维护的开发源码。不同应用使用相同版本的代码;业务适配文件负责字段、数据、权限与扩展接口。
  • 应用不得独立复制并修改组件内部实现、覆盖内部样式后仍声称使用原版。内部行为变更应进入可追踪的 fork。
  • 组件不携带业务数据库、用户会话、密钥或固定业务地址。需要实时数据或配置时,由明确接口提供;不能通过配置悄悄替换可执行代码。

2. 下发完整、固定版本的包

包应包含可读源码、样式与必要资源、最小运行示例、接口说明、组件测试,以及以下维护材料:

  • QUICKSTART.md:最短接入流程、运行方法、已知限制。
  • component.lock.json:组件标识、母版本、来源、文件哈希和依赖版本。
  • API.md:公开参数、事件、业务适配与样式扩展边界。
  • FEATURE.md:新增特性和回送说明模板。
  • 原版基线及分叉记录机制:可以使用 Git 基线或经哈希校验的 .upstream,必须能重建差异。

项目将包和适配代码纳入自己的版本管理,同源部署或原样打包进应用。默认不依赖“latest”地址,不自动升级。构建输出不是另一份手工源码;应能证明实际发布文件来自锁定版本或已登记的 fork。

3. 区分配置、业务扩展与 fork

变更 · 放在哪里 · 是否登记 fork

字段标题、默认排序、现有参数、数据映射 · 应用配置 · 否

保存接口、权限验证、业务字段渲染、已有扩展插槽 · 业务适配层 · 否

新增组件能力、改变内部交互或内部样式、修改公共接口 · 组件开发源 · 是

在具体任务中需要改组件时,先记录母版本、原版哈希、项目和分叉名称,再修改该项目的组件源码。保留原版,不覆盖母版本号,也不以散落的 CSS 补丁规避分叉记录。

4. 项目特性回送母组件

回送材料至少包括:

1. 母版本、分叉标识和原版哈希。

2. 具体使用场景、原行为、新行为,以及不能仅用现有配置实现的原因。

3. 可应用的代码补丁或提交;新增文件和二进制资源也应有完整清单及哈希。

4. 公共接口、默认行为、兼容性和依赖变化。

5. 明确标记的虚构复现示例、测试结果及必要截图。

6. 影响范围、迁移和回滚方式。

回送是提出特性,不是直接覆盖母组件。发送提案、合并或发布按当前任务授权执行,不因为生成补丁就自动执行后续动作。不要附带真实业务数据、会话或凭证。

5. 母组件评估与收敛

母组件维护任务核对基线、检查补丁、复现测试,并记录决定:

  • 通用修复:修复现有能力,保持兼容。
  • 通用新能力:抽象为公开参数或接口,不绑定某个应用。
  • 可选扩展:能力只适合部分场景,保持明确的启用方式。
  • 业务专用:留在应用适配层或该项目分叉,不强行进入母组件。
  • 暂不合并:记录原因或待补充材料。

合并后同步维护源码、接口说明、示例和测试。不要只接受截图或文字结论,也不要让母组件不断积累项目名称判断和业务特例。

6. 验证、发布与升级

  • 组件维护适用于自身的共用交互测试;应用维护数据接口、权限及业务回归测试。使用同一份候选构建包验证代表性应用。
  • UI 组件验证实际点击、键盘、适用的触屏操作、宽窄屏、空值及错误状态;有编辑功能时验证草稿、取消、重复提交和保存失败。只验证 HTTP 状态或文件哈希不能替代交互验证。
  • 部署前校验锁定版本、源码差异与最终构建产物。未登记的修改必须显式处理,不能冒充原版通过发布。
  • 发布新的不可变版本目录、哈希、变更说明和迁移信息,不覆盖旧版本。兼容修复用补丁版本,兼容能力用次版本,不兼容变化用主版本。
  • 各项目主动选择升级。没有本地修改时验证后换版;有 fork 时对原母版、本地改版、新母版做三方合并,再测试部署。
  • 回滚使用项目先前的组件版本与适配配置,不回滚业务数据来掩盖组件问题。

7. 给后续任务的最小上下文

通常只需读取:本规范、组件 QUICKSTART.md、component.lock.json 和应用适配文件。有 fork 时再读取分叉记录、FEATURE.md 与差异补丁。需要实现细节时直接查源码与测试,不要求重新复述整套历史对话。

已实施示例与自动化边界

CRM 基础表格组件已提供固定版本源码包、示例,以及本地安装、校验、登记 fork 和生成提案工具。其当前工具主要跟踪 JS/CSS 两个核心文件;新增资源应补充提案清单与哈希。

这份规范本身不是自动发布系统。其他组件在接入这些工具前,不能宣称已经自动检测修改、自动回送或自动合并。母组件评估、跨版本合并和项目升级仍需明确执行。

新增 2.1:组合条件筛选 · 查看源码与示例

CRM 基础表格组件

固定版本接入

复用控件源码,通过字段配置与业务适配接入。当前公共版本 2.0.0;既有应用保持各自锁定版本,不自动升级。

打开表格组件与接入说明 → · 下载 2.0.0 源码包

记录删除 · 交互规范

独立删除权限 → 显示记录与影响范围 → 确认或取消 → 删除中防重复 → 版本检查 → 成功更新列表 / 失败保留记录。

2.0 已提供通用多选、删除流程与信息日期控件;关联删除、恢复能力与审计策略由应用定义。

阅读完整删除规范 · 查看控件说明

下载 TXT

展开接入提示词

2.0 功能与迁移说明 · 已接入网页与版本清单

水印管理

蓝色工具风与工业工作台共用一套水印:公司文字、时间、透明度、角度、字号和间距可统一调整。默认保持蓝色工具风的低透明度效果。

进入水印管理 →

<script src="https://ai.njwl.ai/module/watermark.js" defer></script>
当前已接入本站 /design/、/design/industrial/ 与 /module/。管理员先预览再保存,已打开页面约 60 秒内同步。其它同源页面可复制上面的脚本;跨域站点需另行配置接口来源。水印用于标识和提醒,不能阻止截图或保证防篡改。

访客 IP 历史管理

本站已有真实服务器访问日志,现提供按日期、完整 IP、页面路径查询和 CSV 导出。

进入访客历史管理 →

每分钟同步,历史仅覆盖现存未压缩日志,查询库保留最近 30 天。可切换页面请求 / 全部请求。IP 是服务器实际连接来源,代理与共享出口不能识别具体人员;请求数不等于真实访客数,也不据此推算停留时间。

IP 明细和全站配置仅管理员可访问;公开页面不返回访客 IP。查询库不保存 URL 查询参数、指纹或精确位置。本站原 Nginx 日志继续沿用原来的轮转策略。

全页面水印

防泄密

内部页面铺一层旋转文字水印(公司 + 内部资料 + 时间),用于来源标识和保密提醒,不阻止截图。不挡点击。

/* 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:.05}
.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、失败红 #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('已复制');
}
要点: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 接口,纯静态页用不了。配合登录态能知道「谁在看」。