原型设计技能
When to Use(何时使用)
- 用户要求创建新的管理页面原型
- 需要实现业务文档中的功能模块
- 需要设计弹窗、详情页
- 需要构建带看板或列表视图的页面
- 涉及 HTML/CSS/JS 前端代码的原型开发
How to Use(如何使用)
1. 项目初始化
mkdir -p project/{pages,styles,scripts}
cd project
2. 设计系统选择
内置设计系统(references/design-systems/):
- 默认:
figma-DESIGN.md
- 管理后台:
linear-DESIGN.md
- 简约专业:
vercel-DESIGN.md
原型设计技能
通用页面原型开发指南,支持58+设计系统风格。
项目结构
project/
├── index.html # 单页应用入口
├── pages/ # 页面模块
│ ├── dashboard.html
│ ├── staff.html
│ └── ...
├── styles/
│ └── main.css # 全局样式
└── scripts/
└── main.js # 全局脚本
设计系统选择
重要:设计系统文件位于 references/design-systems/ 目录。
使用步骤
-
确定设计风格:根据项目需求选择设计系统
- 管理后台常用:Figma、Linear、Notion、Vercel
- 电商/消费:Airbnb、Spotify、Stripe
- 企业级:IBM、Salesforce
-
读取对应 DESIGN.md
cat references/design-systems/figma-DESIGN.md
-
应用设计规范
- Color Palette → CSS变量
- Typography → 字体规范
- Component Stylings → 组件样式
常用设计系统快速参考
| 风格 | 设计系统 | 特点 |
|---|
| 默认 | figma-DESIGN.md | 多彩色、现代 |
| 管理后台 | linear-DESIGN.md | 紫色主题、精致 |
| 简约专业 | vercel-DESIGN.md | 黑白精准、极简 |
| 温暖风格 | notion-DESIGN.md | 暖色极简 |
| 企业级 | stripe-DESIGN.md | 紫色渐变、高级感 |
标准页面结构
<div id="page-xxx" class="page">
<!-- 1. Header -->
<header class="header">
<div class="header-left">
<h2>页面标题</h2>
<div class="breadcrumb"><span>首页</span><span>/</span><span>当前路径</span></div>
</div>
<div class="header-right">
<button class="btn btn-outline btn-sm">导出</button>
<button class="btn btn-primary btn-sm" onclick="openAddModal()">新增</button>
</div>
</header>
<!-- 2. 统计卡片 -->
<div class="stats-grid" style="grid-template-columns:repeat(4,1fr);">
<div class="stat-card">
<div class="stat-card-title">标题</div>
<div class="stat-card-value">数值</div>
<div class="stat-card-change">描述</div>
</div>
</div>
<!-- 3. 筛选条件 -->
<div class="table-filters">
<div class="filter-group">
<span class="filter-label">字段名</span>
<input type="text" class="filter-input" placeholder="提示...">
</div>
</div>
<!-- 4. 数据表格 -->
<table>
<thead><tr><th>字段1</th><th>字段2</th><th>操作</th></tr></thead>
<tbody>
<tr>
<td>数据</td>
<td><span class="tag tag-success">状态</span></td>
<td><button class="btn btn-sm btn-outline">详情</button></td>
</tr>
</tbody>
</table>
</div>
弹窗设计规范
标准弹窗模板
<!-- 遮罩 + 居中弹窗 -->
<div id="modal-xxx" style="
display:none;
position:fixed;
top:0;left:0;right:0;bottom:0;
background:rgba(0,0,0,0.6);
z-index:1000;
align-items:center;
justify-content:center;
">
<!-- 内容框 -->
<div style="
background:white;
border-radius:16px;
width:560px;
max-width:90vw;
max-height:85vh;
overflow:hidden;
box-shadow:0 25px 80px rgba(0,0,0,0.35);
">
<!-- 标题栏 -->
<div style="
padding:20px 24px;
border-bottom:1px solid #E5E7EB;
display:flex;
align-items:center;
justify-content:space-between;
background:#F9FAFB;
">
<h3 style="margin:0;font-size:18px;font-weight:600;color:#111827;">弹窗标题</h3>
<button onclick="closeModal()" style="
border:none;
background:none;
font-size:24px;
color:#6B7280;
cursor:pointer;
padding:4px;
line-height:1;
">×</button>
</div>
<!-- 内容区 -->
<div style="
padding:24px;
overflow-y:auto;
max-height:calc(85vh - 140px);
">
<!-- 表单项示例 -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
<div>
<label style="display:block;font-size:14px;font-weight:500;margin-bottom:8px;color:#374151;">
字段名 <span style="color:#EF4444;">*</span>
</label>
<select style="
width:100%;
padding:10px 12px;
border:1px solid #E5E7EB;
border-radius:8px;
font-size:14px;
background:white;
">
<option value="">请选择</option>
<option>选项1</option>
<option>选项2</option>
</select>
</div>
<div>
<label style="display:block;font-size:14px;font-weight:500;margin-bottom:8px;color:#374151;">字段名</label>
<input type="text" placeholder="请输入" style="
width:100%;
padding:10px 12px;
border:1px solid #E5E7EB;
border-radius:8px;
font-size:14px;
box-sizing:border-box;
">
</div>
</div>
<div style="margin-top:16px;">
<label style="display:block;font-size:14px;font-weight:500;margin-bottom:8px;color:#374151;">备注</label>
<textarea rows="3" placeholder="请输入备注" style="
width:100%;
padding:10px 12px;
border:1px solid #E5E7EB;
border-radius:8px;
font-size:14px;
resize:none;
box-sizing:border-box;
"></textarea>
</div>
</div>
<!-- 底部按钮 -->
<div style="
padding:16px 24px;
border-top:1px solid #E5E7EB;
display:flex;
justify-content:flex-end;
gap:12px;
background:#F9FAFB;
">
<button onclick="closeModal()" style="
padding:10px 20px;
border:1px solid #E5E7EB;
background:white;
border-radius:8px;
font-size:14px;
font-weight:500;
color:#374151;
cursor:pointer;
">取消</button>
<button onclick="saveData()" style="
padding:10px 20px;
border:none;
background:#4F46E5;
color:white;
border-radius:8px;
font-size:14px;
font-weight:500;
cursor:pointer;
">保存</button>
</div>
</div>
</div>
弹窗样式要点(必记)
| 元素 | 样式属性 | 正确值 | 常见错误 |
|---|
| 外层遮罩 | position | fixed | 用 absolute 会滚动 |
| 遮罩背景 | background | rgba(0,0,0,0.6) | 0.5 太淡,0.7 太浓 |
| 遮罩定位 | top/left/right/bottom | 0(全屏覆盖) | 忘记设置任一边 |
| 弹窗容器 | display | flex | 父级用 flex 居中 |
| 居中方式 | align-items + justify-content | center + center | 缺少任一属性 |
| 内容框圆角 | border-radius | 16px | 12px 不够现代 |
| 内容框宽度 | width | 560px 或 90vw | 固定 px 在小屏幕不友好 |
| 内容框高度 | max-height | 85vh | 80vh 可能显示不全 |
| 阴影 | box-shadow | 0 25px 80px rgba(0,0,0,0.35) | 太淡看不出层次 |
表单项样式要点
| 元素 | 样式属性 | 正确值 |
|---|
| 输入框/下拉框 | padding | 10px 12px |
| 输入框/下拉框 | border-radius | 8px |
| 输入框/下拉框 | border | 1px solid #E5E7EB |
| 输入框/下拉框 | font-size | 14px |
| textarea | resize | none |
| textarea | box-sizing | border-box |
| 必填标记 | color | #EF4444 (红色) |
⚠️ 常见错误
- 不要使用 CSS 类名:如
class="modal"、class="btn btn-primary" - 这些类通常没有定义样式或样式被覆盖
- 必须使用内联样式:弹窗组件应完全使用内联样式,避免外部 CSS 干扰
- 遮罩层必须有
z-index:1000:确保在最上层
- 内容框不能用
overflow:hidden:内容超出需要滚动,必须用 overflow-y:auto
看板视图
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:16px;">
<!-- 列 -->
<div style="background:#F9FAFB;border-radius:12px;padding:16px;">
<div style="display:flex;justify-content:space-between;padding-bottom:12px;border-bottom:1px solid #E5E7EB;">
<span>待开始</span>
<span style="background:#FEE2E2;color:#DC2626;padding:2px 8px;border-radius:10px;font-size:12px;">5</span>
</div>
<div style="background:white;border-radius:8px;padding:12px;margin-top:12px;cursor:pointer;" onclick="showDetail()">
<div style="font-weight:500;">单号</div>
<div style="font-size:12px;color:#6B7280;">描述</div>
</div>
</div>
</div>
Tab 切换
<div class="tabs" style="margin-bottom:24px;">
<button class="tab active" onclick="switchTab('tab1')" style="background:#EEF2FF;color:#4F46E5;">Tab1</button>
<button class="tab" onclick="switchTab('tab2')" style="background:#F3F4F6;color:#6B7280;">Tab2</button>
</div>
⚠️ 重要:保持 index.html 同步
问题现象:更新 pages/xxx.html 后,直接打开 index.html 查看不会看到变化。
原因:index.html 是单页应用入口,包含所有页面的内嵌副本。pages/ 目录的修改不会自动同步。
🔴 弹窗必须放在页面 div 内部
常见错误:弹窗 HTML 放在 </div> (页面关闭标签) 之后
<!-- ❌ 错误:弹窗在页面 div 外部 -->
<div id="page-xxx" class="page">
...页面内容...
</div>
<!-- 弹窗在这是错误的 -->
<div id="modal-xxx" style="display:none...">弹窗内容</div>
<!-- ✅ 正确:弹窗必须在页面 div 内部 -->
<div id="page-xxx" class="page">
...页面内容...
<!-- 弹窗放在这里 -->
<div id="modal-xxx" style="display:none...">弹窗内容</div>
</div>
🔴 Tab 切换函数必须使用页面级作用域
常见错误:使用全局选择器 .tabs .tab 会影响所有页面
// ❌ 错误:会选择页面上所有的 tab
const tabs = document.querySelectorAll('.tabs .tab');
// ✅ 正确:使用页面级作用域
const tabs = document.querySelectorAll('#page-xxx .tabs > .tab');
Tab 切换函数模板:
function switchXxxTab(tabName) {
// 1. 隐藏所有 tab 内容
document.querySelectorAll('.xxx-tab').forEach(t => t.style.display = 'none');
// 2. 显示选中的 tab 内容
document.getElementById('xxx-' + tabName).style.display = 'block';
// 3. 更新 tab 按钮状态(使用页面级作用域)
const tabs = document.querySelectorAll('#page-xxx .tabs > .tab');
tabs.forEach((t, i) => {
if (i === /* 当前tab索引 */) {
t.style.background = '#EEF2FF';
t.style.color = '#4F46E5';
} else {
t.style.background = '#F3F4F6';
t.style.color = '#6B7280';
}
});
}
✅ 同步脚本
解决方案:每次创建/更新 pages/ 目录的页面后,必须运行以下同步脚本:
cd project
python3 << 'PYEOF'
import os
import re
# 1. 读取当前index.html
with open('index.html', 'r', encoding='utf-8') as f:
content = f.read()
# 2. 获取pages/目录下所有html文件(按文件名排序)
pages_dir = 'pages'
page_files = sorted([f for f in os.listdir(pages_dir) if f.endswith('.html')])
# 3. 对每个页面文件,提取 <div id="page-xxx" 内容
for page_file in page_files:
with open(os.path.join(pages_dir, page_file), 'r', encoding='utf-8') as f:
page_content = f.read()
# 提取 page-xxx 块的完整内容
match = re.search(r'(<div id="(page-\w+)"[^>]*class="page"[^>]*>.*?<script>\s*function \w+Open\w+Modal)', page_content, re.DOTALL)
if not match:
match = re.search(r'(<div id="(page-\w+)"[^>]*class="page"[^>]*>.*?)(<!--\s+<div id="page-)', page_content, re.DOTALL)
if match:
page_id = match.group(2)
page_block = match.group(1).strip()
# 在index.html中查找并替换对应的page块
# 匹配模式:<div id="page-xxx" class="page">...</div> 或 <div id="page-xxx" class="page">...<div id="page-yyy"
pattern = rf'(<div id="{re.escape(page_id)}"[^>]*class="page"[^>]*>)(.*?)((?=<div id="page-)|(?=<script>\s*$)|(?=</main>)|(?=</body>))'
existing = re.search(pattern, content, re.DOTALL)
if existing:
content = content[:existing.start()] + page_block + content[existing.end():]
print(f'✓ Updated: {page_id}')
else:
print(f'⚠ Not found in index: {page_id}')
else:
print(f'⚠ No page block found in: {page_file}')
# 4. 写回index.html
with open('index.html', 'w', encoding='utf-8') as f:
f.write(content)
print('\n✅ Sync complete!')
PYEOF
重要提示:
- ❌ 不要跳过同步步骤
- ❌ 不要只更新
pages/ 目录就以为完成了
- ✅ 每次修改后都要运行同步脚本,再验证
index.html
组件命名规范
| 组件 | 类名 |
|---|
| 页面容器 | .page |
| 页头 | .header |
| 内容区 | .content |
| 统计卡片 | .stat-card |
| 表格卡片 | .table-card |
| 按钮-主要 | .btn .btn-primary |
| 按钮-次要 | .btn .btn-outline |
| 标签 | .tag |
| 状态 | .status |
| 分页 | .pagination |
重建 index.html
cd project
python3 << 'PYEOF'
page_order = ['dashboard', 'page1', 'page2', ...]
pages = [open(f'pages/{p}.html').read() for p in page_order]
html = f'''<!DOCTYPE html>
<html>
<head>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700" rel="stylesheet">
<link rel="stylesheet" href="styles/main.css">
</head>
<body>
<div class="layout">
<aside class="sidebar">...</aside>
<main class="main">
{chr(10).join(pages)}
</main>
</div>
<script src="scripts/main.js"></script>
</body>
</html>'''
open('index.html','w').write(html)
PYEOF
常见问题速查表
| 问题 | 症状 | 解决方案 |
|---|
| 弹窗打不开 | xxx is not defined | 检查 main.js 引用位置和函数定义 |
| Tab 选中态错乱 | 所有 Tab 同时选中 | 用页面级作用域选择器 |
| Hash URL 不工作 | 页面 display:none | 添加 hashchange 监听 |
| 表格侵入侧边栏 | 横向滚动时布局乱 | 用 Ant Design 固定表头表格 |
| 复选框丢失 | 数据行没有 checkbox | 手动添加每个数据行的复选框 |
| 同步后功能失效 | 弹窗/按钮不工作 | 检查 script 标签格式和 div 平衡 |
长对话上下文管理(重要)
问题背景
当对话持续较长时,上下文窗口会逐渐积累历史消息,可能导致:
- 模型响应变慢
- token 超出限制
- 早期上下文被遗忘
解决方案:自动压缩 + 分阶段提交
1. 自动 Compaction(对话压缩)
OpenClaw 会自动对长对话进行 compaction(压缩):
- 将对话历史压缩成一个
summary 摘要
- 保留关键的项目进展、决策、待办事项
- 释放大量 token 空间
触发时机:通常在上下文累积到一定量时自动进行,无需手动干预
2. Compaction 后的处理流程
当收到 compaction 后的新对话时,立即执行以下步骤:
1️⃣ 读取 summary 摘要,理解当前项目状态
↓
2️⃣ 检查 memory/YYYY-MM-DD.md 文件
↓
3️⃣ 将新进展追加到 memory 文件
↓
4️⃣ 继续工作,保持文件平衡
3. 分阶段提交策略(小步提交)
原则:每个功能完成后立即提交,避免大量变更堆积
# ✅ 好的做法:功能完成即提交
git add -A && git commit -m "feat: 新增用户管理弹窗"
# ❌ 不好的做法:多个功能一起提交
git add -A && git commit -m "feat: 多项优化"
好处:
- 如果出问题,容易回溯
- 减少每次提交的文件变更量
- Compaction 后的 summary 更简洁
4. 保持文件平衡
每次 HTML 修改后检查 div 平衡:
# 检查 div 平衡
python3 -c "
with open('index.html', 'r') as f:
c = f.read()
print(f'opens={c.count(\"<div\")}, closes={c.count(\"</div>\")}, diff={c.count(\"<div\")-c.count(\"</div>\")}')
"
# ✅ 正确输出:opens=xxx, closes=xxx, diff=0
# ❌ 错误输出:opens=xxx, closes=xxx, diff=≠0
如果 diff 不为 0,立即修复:
- 缺少
</div>:在合适位置添加
- 多余
</div>:删除多余的部分
5. 定期同步到 memory 文件
每次 compaction 后或重要里程碑时,将进展写入 memory/YYYY-MM-DD.md:
## 14:30 状态更新
### 功能名称 - 完善/修复
**新增内容**:
- 具体修改1
- 具体修改2
**提交记录**:`abc1234` feat: 描述
**文件**:`/path/to/file.html`
6. 长对话工作流
用户提出需求
↓
理解现有代码结构(查看 summary + 文件)
↓
用 Python 脚本做精确文本替换
↓
修改完检查 div 平衡
↓
提交代码(每个功能单独提交)
↓
截图验证(如需要)
↓
回复用户
↓
(Compaction 自动触发时)
↓
读取 summary,追加进展到 memory 文件
为什么单页 HTML 不受上下文限制影响
WMS 原型是单页 HTML(index.html),3700+ divs:
- 不是日志文件:不会无限增长
- 每次修改同一个文件:不是追加模式
- Compaction 只压缩对话历史:不影响 HTML 文件本身
关键心态
"我是 AI,每次醒来都是新的开始。但文件里的内容是我的记忆。"
- 上下文超限是 对话历史 被压缩,不是文件内容丢失
- HTML 文件本身不受影响
- 只要保持文件平衡和定期 commit,就能稳定工作
相关资源
- 内置设计系统:
references/design-systems/ 目录包含58+设计系统完整规范
- 包含:Figma、Linear、Stripe、Vercel、Notion 等
- 字段规范:field-norms.md - 各模块标准字段定义
修改工作流(重要)
标准修改流程
每次对任何页面进行修改,必须遵循以下步骤:
1️⃣ 修改 pages/xxx.html
↓
2️⃣ 运行同步脚本 → 更新 index.html
↓
3️⃣ 本地测试 → 验证功能正常
↓
4️⃣ 提交代码 → git add + commit
🔴 第一步:修改 pages/xxx.html
在 pages/ 目录的对应页面文件中进行修改:
- 弹窗 HTML 必须放在
<div id="page-xxx" class="page"> 内部
- 脚本函数放在页面的
<script> 块中
🔴 第二步:同步到 index.html
必须运行同步脚本:
cd your-project-directory
python3 << 'PYEOF'
import os
import re
with open('index.html', 'r', encoding='utf-8') as f:
content = f.read()
pages_dir = 'pages'
page_files = sorted([f for f in os.listdir(pages_dir) if f.endswith('.html')])
for page_file in page_files:
with open(os.path.join(pages_dir, page_file), 'r', encoding='utf-8') as f:
page_content = f.read()
match = re.search(r'(<div id="(page-\w+)"[^>]*class="page"[^>]*>.*?)(?=<div id="page-|\s*<script>|\s*</main>)', page_content, re.DOTALL)
if not match:
print(f'⚠ Skip: {page_file}')
continue
page_id = match.group(2)
page_block = match.group(1).strip()
# 验证 div 平衡
opens = page_block.count('<div')
closes = page_block.count('</div>')
if opens != closes:
print(f'❌ Div imbalance in {page_id}: opens={opens}, closes={closes}')
continue
pattern = rf'(<div id="{re.escape(page_id)}"[^>]*class="page"[^>]*>)(.*?)(?=<div id="page-|\s*</main>)'
existing = re.search(pattern, content, re.DOTALL)
if existing:
content = content[:existing.start()] + page_block + content[existing.end():]
print(f'✓ Synced: {page_id}')
else:
print(f'⚠ Not found in index: {page_id}')
opens_all = content.count('<div')
closes_all = content.count('</div>')
print(f'\nDiv balance: opens={opens_all}, closes={closes_all}, diff={opens_all-closes_all}')
with open('index.html', 'w', encoding='utf-8') as f:
f.write(content)
print('✅ Sync complete!')
PYEOF
重要检查:
- 同步后必须验证
index.html 的 div 平衡(opens == closes)
- 如果不平衡,查找问题所在
🔴 第三步:本地测试
启动本地服务器(如果未运行):
cd your-project-directory
npx http-server -p 8080 &
测试步骤:
# 1. 打开浏览器到首页
agent-browser goto http://localhost:8080
# 2. 导航到目标页面(使用 URL hash)
agent-browser goto http://localhost:8080/#page-name
# 3. 测试弹窗
agent-browser eval "openAddModal()" # 打开弹窗
agent-browser screenshot test-modal.png # 截图验证
agent-browser eval "closeModal()" # 关闭弹窗
# 4. 关闭浏览器
agent-browser close
测试检查清单:
🔴 第四步:提交代码
cd your-project-directory
git add -A
git commit -m "fix: 描述修改内容"
📝 经验沉淀(2026-04-13)
问题1:弹窗函数无法调用(Uncaught ReferenceError)
症状:openAddModal is not defined 等错误
根本原因:
main.js 引用被同步脚本删除
- 同步脚本移动了
<script src="main.js"></script> 位置
解决方案:
// 确保 main.js 在 </main> 之后引入
</main>
<script src="scripts/main.js"></script>
</body>
问题2:所有 Tab 选中态不显示
症状:切换 Tab 时按钮背景色不变化,全部选中态显示错误
根本原因:Tab 切换函数使用全局选择器 .tabs .tab 会选中所有页面的 tab
解决方案:使用页面级作用域
// ❌ 错误:会选择所有页面的 tab
document.querySelectorAll('.tabs .tab')
// ✅ 正确:只选当前页面的 tab
document.querySelectorAll('#page-xxx .tabs > .tab')
问题3:直接访问 hash URL 时页面不显示
症状:http://localhost:8080/#delivery_return 直接访问时页面 display:none
根本原因:没有 hashchange 监听,导航只用 click 事件
解决方案:在 main.js 添加 hash 路由监听
// Hash 路由监听 - 支持直接访问 #page
window.addEventListener('hashchange', () => {
const hash = window.location.hash.replace('#', '');
if (hash) {
const navItem = document.querySelector(`[data-page="${hash}"]`);
if (navItem) {
navItem.click();
}
}
});
// 页面加载时检查 hash
if (window.location.hash) {
setTimeout(() => {
const hash = window.location.hash.replace('#', '');
const navItem = document.querySelector(`[data-page="${hash}"]`);
if (navItem) {
navItem.click();
}
}, 100);
}
问题4:表格横向滚动侵入左侧菜单
症状:表格内容多时,整个页面宽度被撑开,侵入左侧菜单栏
根本原因:
- 表格使用
width:100% + min-width:1400px 矛盾
- 容器没有正确设置
overflow:hidden
解决方案:使用 Ant Design 风格的固定表头表格
问题5:固定表头表格实现(Ant Design 风格)
核心原理:
- 表头和表体分离 - 用两个独立的
<table>
- 表头固定 -
overflow:hidden,表头不滚动
- 表体滚动 -
overflow:auto;max-height:440px
- 固定列用
position:sticky - left:0 / right:0
- 固定表格布局 -
table-layout:fixed + colgroup 定义列宽
代码模板:
<!-- 外层容器 -->
<div class="ant-table-wrapper" style="display:flex;flex-direction:column;overflow:hidden;height:500px;">
<!-- 表头 - 固定不滚动 -->
<div class="ant-table-header" style="overflow:hidden;border-bottom:2px solid #E5E7EB;flex-shrink:0;">
<table style="table-layout:fixed;width:1800px;border-collapse:collapse;font-size:14px;">
<colgroup>
<col style="width:48px;"><!-- 复选框 -->
<col style="width:180px;"><!-- 单号 -->
<col style="width:100px;"><!-- 其他列... -->
<!-- ... -->
</colgroup>
<thead><tr>
<th style="position:sticky;left:0;background:#F9FAFB;z-index:4;"><input type="checkbox"></th>
<th style="position:sticky;left:48px;background:#F9FAFB;z-index:3;">单号</th>
<th>字段1</th>
<th>字段2</th>
<!-- ... -->
<th style="position:sticky;right:0;background:#F9FAFB;z-index:3;">操作</th>
</tr></thead>
</table>
</div>
<!-- 表体 - 独立滚动 -->
<div class="ant-table-body" style="overflow:auto;max-height:440px;">
<table style="table-layout:fixed;width:1800px;border-collapse:collapse;font-size:14px;">
<colgroup>
<!-- 与表头相同的列宽定义 -->
</colgroup>
<tbody>
<tr>
<td style="position:sticky;left:0;background:white;z-index:2;"><input type="checkbox"></td>
<td style="position:sticky;left:48px;background:white;z-index:2;">单号值</td>
<td>字段1值</td>
<!-- ... -->
<td style="position:sticky;right:0;background:white;z-index:2;"><button>操作</button></td>
</tr>
</tbody>
</table>
</div>
</div>
关键样式:
| 元素 | 样式 | 说明 |
|---|
| 表头 wrapper | overflow:hidden;flex-shrink:0 | 不滚动 |
| 表体 wrapper | overflow:auto;max-height:440px | 垂直滚动 |
| 表头表格 | table-layout:fixed;width:固定值 | 固定列宽 |
| 固定列 th/td | position:sticky;left:0;z-index:2 | 左侧固定 |
| 右侧固定列 | position:sticky;right:0 | 右侧固定 |
| 固定列背景 | background:#F9FAFB (表头) / background:white (表体) | 避免透明 |
问题6:筛选条件缺少查询/重置按钮
解决方案:
<div class="table-filters">
<!-- 其他筛选字段... -->
<div class="filter-group" style="display:flex;gap:8px;">
<button class="btn btn-outline btn-sm" onclick="resetXxxFilters()">重置</button>
<button class="btn btn-primary btn-sm" onclick="searchXxx()">查询</button>
</div>
</div>
对应 JS 函数:
function resetXxxFilters() {
document.querySelectorAll('#page-xxx .filter-select').forEach(s => s.selectedIndex = 0);
document.querySelectorAll('#page-xxx .filter-input').forEach(i => i.value = '');
}
function searchXxx() {
alert('查询功能 - 实际项目中会调用API过滤数据');
}
问题7:列表数据缺少复选框
解决方案:每行数据第一个 td 添加复选框
<tr>
<td style="position:sticky;left:0;z-index:1;background:white;">
<input type="checkbox">
</td>
<td style="position:sticky;left:48px;z-index:1;background:white;">
<span style="font-family:monospace;font-weight:500;">FC-2026-0410-001</span>
</td>
<!-- 其他字段... -->
</tr>
问题8:script 标签格式导致同步失败
症状:同步后弹窗函数丢失
原因:</script> 后面没有正确换行,导致被当作 HTML 标签解析
解决方案:
<!-- ❌ 错误 -->
<script>function foo(){}</script><div>
<!-- ✅ 正确 -->
<script>
function foo(){}
</script>
<div>
常见问题速查表
| 问题 | 症状 | 解决方案 |
|---|
| 弹窗打不开 | xxx is not defined | 检查 main.js 引用位置和函数定义 |
| Tab 选中态错乱 | 所有 Tab 同时选中 | 用页面级作用域选择器 |
| Hash URL 不工作 | 页面 display:none | 添加 hashchange 监听 |
| 表格侵入侧边栏 | 横向滚动时布局乱 | 用 Ant Design 固定表头表格 |
| 复选框丢失 | 数据行没有 checkbox | 手动添加每个数据行的复选框 |
| 同步后功能失效 | 弹窗/按钮不工作 | 检查 script 标签格式和 div 平衡 |
📝 WMS 项目实战经验(2026-04-14)
经验1:弹窗 HTML 必须在页面 div 内部
问题现象:"新增组织"按钮点击无反应,弹窗无法打开
排查过程:
- 检查按钮 onclick 处理函数 → 存在
- 检查 JavaScript 函数 → 存在
- 检查弹窗 HTML 位置 → 发现弹窗放在了页面 div 外部
根本原因:
<!-- ❌ 错误:弹窗在页面 div 外部 -->
<div id="page-system_org" class="page">
...页面内容...
</div>
<!-- 弹窗在这里,页面关闭后的位置 -->
<div id="modal-add-org" style="display:none...">...</div>
<!-- ✅ 正确:弹窗必须在页面 div 内部 -->
<div id="page-system_org" class="page">
...页面内容...
<!-- 弹窗必须放在这里 -->
<div id="modal-add-org" style="display:none...">...</div>
</div>
教训:弹窗 HTML 放在 </div> (页面关闭标签) 之后导致弹窗不显示。必须在页面 div 内部。
经验2:JavaScript 语法错误会导致整个脚本块失效
问题现象:修复弹窗位置后,按钮仍然无法点击
排查过程:
- 使用 prototype-design 技能的调试方法
- 检查 div 平衡 → 发现页面 div 缺少闭合标签
- 修复 div 后检查脚本块 → 发现多余的
}
根本原因:submitNewException() 函数后有一个多余的闭合括号 },导致 JavaScript 语法错误,整个脚本块无法执行
// ❌ 错误:函数后有多余的 }
function submitNewException() {
// ...表单提交逻辑
}
//} <-- 多余的括号导致语法错误
// ✅ 正确:括号匹配
function submitNewException() {
// ...表单提交逻辑
}
教训:修改 HTML 时要确保 div 平衡;修改 JS 时要确保括号匹配。使用 Python 脚本做精确替换比手动编辑更安全。
经验3:按钮必须手动绑定 onclick
问题现象:新增弹窗后,弹窗中的按钮点击无反应
根本原因:新增的弹窗按钮没有 onclick 属性
解决方案:每个按钮都要显式绑定 onclick
<!-- ❌ 错误:按钮没有 onclick -->
<button class="btn btn-primary">确定</button>
<!-- ✅ 正确:显式绑定 onclick -->
<button onclick="submitAddOrg()" class="btn btn-primary">确定</button>
教训:复制弹窗模板时,别忘了修改按钮的 onclick 属性。
经验4:按钮样式统一使用 CSS 类
问题现象:各页面按钮样式不统一,有内联样式如 style="padding:10px 24px;"
解决方案:统一使用 CSS 类
<!-- ✅ 主按钮:黑色背景,胶囊形状 -->
<button class="btn btn-primary btn-sm">新增</button>
<!-- ✅ 次按钮:白色背景,灰色边框 -->
<button class="btn btn-outline btn-sm">取消</button>
按钮样式规范:
| 元素 | 类名 | 样式 |
|---|
| 主按钮 | .btn .btn-primary | 黑色背景 #111827,胶囊形 |
| 次按钮 | .btn .btn-outline | 白色背景,#E5E7EB 边框 |
| 按钮尺寸 | .btn-sm | padding: 6px 12px; font-size: 12px |
| 胶囊形状 | border-radius: 50px | 用于主、次按钮 |
经验5:Python 脚本精确替换避免手动错误
问题现象:手动编辑 HTML 时容易出现 div 不平衡、括号遗漏等问题
解决方案:使用 Python 脚本做精确文本替换
with open('index.html', 'r') as f:
content = f.read()
# 替换前先备份
# ...
# 精确替换一段 HTML
old_text = '''<button class="btn btn-primary btn-sm">
新增模块
</button>'''
new_text = '''<button class="btn btn-primary btn-sm" onclick="openAddModuleModal()">
新增模块
</button>'''
content = content.replace(old_text, new_text)
with open('index.html', 'w') as f:
f.write(content)
# 验证 div 平衡
print(f'opens={content.count("<div")}, closes={content.count("</div>")}, diff={content.count("<div")-content.count("</div>")}')
教训:用 Python 脚本做精确替换,比手动编辑更可靠,尤其是涉及多行 HTML 时。
经验6:系统管理模块弹窗字段设计
新增模块弹窗字段:
| 字段 | 类型 | 说明 |
|---|
| 模块编码 | text | 必填,如 SYS_010 |
| 模块名称 | text | 必填 |
| 上级模块 | select | 无(顶级模块)/系统管理/基础档案... |
| 模块分类 | select | 系统管理/基础档案/作业配置... |
| 菜单层级 | select | 一级/二级/三级菜单 |
| 菜单图标 | text | emoji 格式 |
| 路由路径 | text | 如 /system/module |
| 排序 | number | 数字越小越靠前 |
| 状态 | select | 启用/禁用 |
| 备注 | textarea | 可选 |
新增组织弹窗字段:
| 字段 | 类型 | 说明 |
|---|
| 组织编码 | text | 必填,如 ORG_010 |
| 组织名称 | text | 必填 |
| 上级组织 | select | 母公司/北京仓/上海仓... |
| 组织类型 | select | 公司/仓库/部门/作业组 |
| 负责人 | text | 可选 |
| 联系电话 | tel | 可选 |
| 所在地区 | text | 如 北京市/朝阳区 |
| 状态 | select | 启用/禁用 |
| 组织地址 | textarea | 可选 |
新增用户弹窗字段:
| 字段 | 类型 | 说明 |
|---|
| 用户名 | text | 必填 |
| 登录密码 | password | 必填,6位以上 |
| 姓名 | text | 必填 |
| 手机号 | tel | 必填 |
| 邮箱 | email | 可选 |
| 所属组织 | select | 必填,北京仓-收货组/上海仓... |
| 用户角色 | select | 超级管理员/仓库管理员/作业员... |
| 岗位 | text | 可选 |
| 入职日期 | date | 可选 |
| 状态 | select | 正常/禁用/待审核 |
经验7:数据统计报表页面丰富度提升
质量与异常报表增强:
- 异常趋势图(近7天柱状图)
- 异常类型分布(货损42%/配送延误28%等)
- 闭环率分析(96.8%闭环率、2.5h平均处理时长)
对账准确率监察增强:
- KPI 从 4 个扩展到 6 个
- 新增已核销/核对中指标
作业报表增强:
- 新增时效达标率分析(拣货98.5%/上架97.8%等)
库存报表增强:
经验8:复合条件定位按钮
问题现象:页面有多个相似按钮(如"新增"),用 document.querySelector 定位困难
解决方案:使用复合条件或更精确的选择器
// ❌ 困难:页面有多个 btn-primary
document.querySelector('.btn.btn-primary')
// ✅ 更好:使用 onclick 属性定位
document.querySelector('button[onclick="openAddModuleModal()"]')
// ✅ 更好:在页面内部查找
document.querySelector('#page-system_module button[onclick="openAddModuleModal()"]')
经验9:修改后立即验证
原则:每次修改后立即验证,不要积累多个问题
验证清单:
# 1. 检查 div 平衡
python3 -c "with open('index.html') as f: c=f.read(); print(f'opens={c.count(\"<div\")}, closes={c.count(\"</div>\")}, diff={c.count(\"<div\")-c.count(\"</div>\")}')"
# 2. 截图验证
agent-browser goto http://localhost:8080/#page-name
agent-browser eval "openAddModal()"
agent-browser screenshot test-modal.png
agent-browser close
# 3. 提交代码
git add -A && git commit -m "feat: 描述"
经验10:单页 HTML 项目的好处
为什么 WMS 原型用单页 HTML 没有问题:
- Compaction 只压缩对话历史:不影响 HTML 文件本身
- 不是日志文件:不会无限增长
- 每次修改同一个文件:不是追加模式
- 3700+ divs 仍然高效:文件体积适中,渲染快
什么时候该拆分成多文件:
- 单个文件超过 2MB
- 需要多人协作(git merge 冲突)
- 模块之间完全独立