通用组件采用固定版本源码包与业务适配,按下方开发规范维护分叉和特性回送。历史代码片段保留供参考,逐步组件化。
固定版本下发 → 项目按需 fork → 特性回送 → 母组件评估 → 新版本发布 → 项目选择升级。
适用于表格、弹窗、上传、文件浏览、导航等组件。应用使用可执行组件与业务适配,不按长提示词重新实现控件。
规范版本:1.0 · 更新:2026-09-12。适用于表格、弹窗、上传、文件浏览、导航等可复用组件。默认采用“固定版本下发 → 项目按需 fork → 特性回送 → 母组件评估 → 发布新版本 → 项目选择升级”。
这是新建及后续改造组件的开发规范,不代表 module 中所有历史代码片段已经完成组件化。表格组件是首个实施示例;各组件应明确标注自己的封装、工具与验证状态。
包应包含可读源码、样式与必要资源、最小运行示例、接口说明、组件测试,以及以下维护材料:
QUICKSTART.md:最短接入流程、运行方法、已知限制。component.lock.json:组件标识、母版本、来源、文件哈希和依赖版本。API.md:公开参数、事件、业务适配与样式扩展边界。FEATURE.md:新增特性和回送说明模板。.upstream,必须能重建差异。项目将包和适配代码纳入自己的版本管理,同源部署或原样打包进应用。默认不依赖“latest”地址,不自动升级。构建输出不是另一份手工源码;应能证明实际发布文件来自锁定版本或已登记的 fork。
变更 · 放在哪里 · 是否登记 fork
字段标题、默认排序、现有参数、数据映射 · 应用配置 · 否
保存接口、权限验证、业务字段渲染、已有扩展插槽 · 业务适配层 · 否
新增组件能力、改变内部交互或内部样式、修改公共接口 · 组件开发源 · 是
在具体任务中需要改组件时,先记录母版本、原版哈希、项目和分叉名称,再修改该项目的组件源码。保留原版,不覆盖母版本号,也不以散落的 CSS 补丁规避分叉记录。
回送材料至少包括:
1. 母版本、分叉标识和原版哈希。
2. 具体使用场景、原行为、新行为,以及不能仅用现有配置实现的原因。
3. 可应用的代码补丁或提交;新增文件和二进制资源也应有完整清单及哈希。
4. 公共接口、默认行为、兼容性和依赖变化。
5. 明确标记的虚构复现示例、测试结果及必要截图。
6. 影响范围、迁移和回滚方式。
回送是提出特性,不是直接覆盖母组件。发送提案、合并或发布按当前任务授权执行,不因为生成补丁就自动执行后续动作。不要附带真实业务数据、会话或凭证。
母组件维护任务核对基线、检查补丁、复现测试,并记录决定:
合并后同步维护源码、接口说明、示例和测试。不要只接受截图或文字结论,也不要让母组件不断积累项目名称判断和业务特例。
通常只需读取:本规范、组件 QUICKSTART.md、component.lock.json 和应用适配文件。有 fork 时再读取分叉记录、FEATURE.md 与差异补丁。需要实现细节时直接查源码与测试,不要求重新复述整套历史对话。
CRM 基础表格组件已提供固定版本源码包、示例,以及本地安装、校验、登记 fork 和生成提案工具。其当前工具主要跟踪 JS/CSS 两个核心文件;新增资源应补充提案清单与哈希。
这份规范本身不是自动发布系统。其他组件在接入这些工具前,不能宣称已经自动检测修改、自动回送或自动合并。母组件评估、跨版本合并和项目升级仍需明确执行。
复用控件源码,通过字段配置与业务适配接入。当前公共版本 2.0.0;既有应用保持各自锁定版本,不自动升级。
独立删除权限 → 显示记录与影响范围 → 确认或取消 → 删除中防重复 → 版本检查 → 成功更新列表 / 失败保留记录。
2.0 已提供通用多选、删除流程与信息日期控件;关联删除、恢复能力与审计策略由应用定义。
下载 TXT蓝色工具风与工业工作台共用一套水印:公司文字、时间、透明度、角度、字号和间距可统一调整。默认保持蓝色工具风的低透明度效果。
<script src="https://ai.njwl.ai/module/watermark.js" defer></script>本站已有真实服务器访问日志,现提供按日期、完整 IP、页面路径查询和 CSV 导出。
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 秒刷新;如需显示「谁在看」可接登录用户信息。保存 / 复制成功等操作后,屏幕中间弹一条提示,自动消失。
/* 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 接口,纯静态页用不了。配合登录态能知道「谁在看」。