通用页面原型设计技能。基于 58种设计风格(默认Figma 风格) UI 的复杂业务系统 HTML 原型开发, 包括标准页面结构、统计卡片、筛选条件、数据表格、弹窗设计。 适用场景:(1) 创建新的管理页面原型 (2) 对照业务文档实现功能模块 (3) 设计弹窗和详情页 (4) 构建带看板和列表视图的页面。 **...
---
name: prototype-design
description: |
通用页面原型设计技能。基于 58种设计风格(默认Figma 风格) UI 的复杂业务系统 HTML 原型开发,
包括标准页面结构、统计卡片、筛选条件、数据表格、弹窗设计。
适用场景:(1) 创建新的管理页面原型 (2) 对照业务文档实现功能模块
(3) 设计弹窗和详情页 (4) 构建带看板和列表视图的页面。
**样式选择**:内置58+设计系统,直接引用 `references/design-systems/` 目录。
使用时指定 DESIGN_SYSTEM=<名称> 即可应用对应设计风格。
origin: openclaw
last_updated: 2026-04-14
changelog:
- 2026-04-14: 初始版本,从 WMS 项目经验中沉淀
- 2026-04-14: 新增长对话上下文管理章节
- 2026-04-14: 新增 WMS 项目实战经验(10条核心教训)
---
# 原型设计技能
## When to Use(何时使用)
- 用户要求创建新的管理页面原型
- 需要实现业务文档中的功能模块
- 需要设计弹窗、详情页
- 需要构建带看板或列表视图的页面
- 涉及 HTML/CSS/JS 前端代码的原型开发
## How to Use(如何使用)
### 1. 项目初始化
```bash
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/` 目录。
### 使用步骤
1. **确定设计风格**:根据项目需求选择设计系统
- 管理后台常用:Figma、Linear、Notion、Vercel
- 电商/消费:Airbnb、Spotify、Stripe
- 企业级:IBM、Salesforce
2. **读取对应 DESIGN.md**
```bash
cat references/design-systems/figma-DESIGN.md
```
3. **应用设计规范**
- Color Palette → CSS变量
- Typography → 字体规范
- Component Stylings → 组件样式
### 常用设计系统快速参考
| 风格 | 设计系统 | 特点 |
|------|---------|------|
| 默认 | `figma-DESIGN.md` | 多彩色、现代 |
| 管理后台 | `linear-DESIGN.md` | 紫色主题、精致 |
| 简约专业 | `vercel-DESIGN.md` | 黑白精准、极简 |
| 温暖风格 | `notion-DESIGN.md` | 暖色极简 |
| 企业级 | `stripe-DESIGN.md` | 紫色渐变、高级感 |
## 标准页面结构
```html
<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>
```
## 弹窗设计规范
### 标准弹窗模板
```html
<!-- 遮罩 + 居中弹窗 -->
<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` (红色) |
### ⚠️ 常见错误
1. **不要使用 CSS 类名**:如 `class="modal"`、`class="btn btn-primary"` - 这些类通常没有定义样式或样式被覆盖
2. **必须使用内联样式**:弹窗组件应完全使用内联样式,避免外部 CSS 干扰
3. **遮罩层必须有 `z-index:1000`**:确保在最上层
4. **内容框不能用 `overflow:hidden`**:内容超出需要滚动,必须用 `overflow-y:auto`
## 看板视图
```html
<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 切换
```html
<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>` (页面关闭标签) **之后**
```html
<!-- ❌ 错误:弹窗在页面 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` 会影响所有页面
```javascript
// ❌ 错误:会选择页面上所有的 tab
const tabs = document.querySelectorAll('.tabs .tab');
// ✅ 正确:使用页面级作用域
const tabs = document.querySelectorAll('#page-xxx .tabs > .tab');
```
**Tab 切换函数模板**:
```javascript
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/` 目录的页面后,必须运行以下同步脚本:
```bash
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
```bash
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. 分阶段提交策略(小步提交)
**原则**:每个功能完成后立即提交,避免大量变更堆积
```bash
# ✅ 好的做法:功能完成即提交
git add -A && git commit -m "feat: 新增用户管理弹窗"
# ❌ 不好的做法:多个功能一起提交
git add -A && git commit -m "feat: 多项优化"
```
**好处**:
- 如果出问题,容易回溯
- 减少每次提交的文件变更量
- Compaction 后的 summary 更简洁
#### 4. 保持文件平衡
每次 HTML 修改后检查 div 平衡:
```bash
# 检查 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`:
```markdown
## 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](references/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
**必须运行同步脚本**:
```bash
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)
- 如果不平衡,查找问题所在
### 🔴 第三步:本地测试
**启动本地服务器**(如果未运行):
```bash
cd your-project-directory
npx http-server -p 8080 &
```
**测试步骤**:
```bash
# 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
```
**测试检查清单**:
- [ ] 页面正常加载
- [ ] 弹窗能打开
- [ ] 弹窗内容正确
- [ ] 弹窗能关闭
- [ ] Tab 切换正常(如有)
- [ ] 按钮点击有效
### 🔴 第四步:提交代码
```bash
cd your-project-directory
git add -A
git commit -m "fix: 描述修改内容"
```
---
## 📝 经验沉淀(2026-04-13)
### 问题1:弹窗函数无法调用(Uncaught ReferenceError)
**症状**:`openAddModal is not defined` 等错误
**根本原因**:
1. `main.js` 引用被同步脚本删除
2. 同步脚本移动了 `<script src="main.js"></script>` 位置
**解决方案**:
```javascript
// 确保 main.js 在 </main> 之后引入
</main>
<script src="scripts/main.js"></script>
</body>
```
### 问题2:所有 Tab 选中态不显示
**症状**:切换 Tab 时按钮背景色不变化,全部选中态显示错误
**根本原因**:Tab 切换函数使用全局选择器 `.tabs .tab` 会选中**所有页面**的 tab
**解决方案**:使用页面级作用域
```javascript
// ❌ 错误:会选择所有页面的 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 路由监听
```javascript
// 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:表格横向滚动侵入左侧菜单
**症状**:表格内容多时,整个页面宽度被撑开,侵入左侧菜单栏
**根本原因**:
1. 表格使用 `width:100%` + `min-width:1400px` 矛盾
2. 容器没有正确设置 `overflow:hidden`
**解决方案**:使用 Ant Design 风格的固定表头表格
### 问题5:固定表头表格实现(Ant Design 风格)
**核心原理**:
1. **表头和表体分离** - 用两个独立的 `<table>`
2. **表头固定** - `overflow:hidden`,表头不滚动
3. **表体滚动** - `overflow:auto;max-height:440px`
4. **固定列用 `position:sticky`** - `left:0` / `right:0`
5. **固定表格布局** - `table-layout:fixed` + colgroup 定义列宽
**代码模板**:
```html
<!-- 外层容器 -->
<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:筛选条件缺少查询/重置按钮
**解决方案**:
```html
<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 函数**:
```javascript
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 添加复选框
```html
<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 标签解析
**解决方案**:
```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 内部
**问题现象**:"新增组织"按钮点击无反应,弹窗无法打开
**排查过程**:
1. 检查按钮 onclick 处理函数 → 存在
2. 检查 JavaScript 函数 → 存在
3. 检查弹窗 HTML 位置 → **发现弹窗放在了页面 div 外部**
**根本原因**:
```html
<!-- ❌ 错误:弹窗在页面 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 语法错误会导致整个脚本块失效
**问题现象**:修复弹窗位置后,按钮仍然无法点击
**排查过程**:
1. 使用 prototype-design 技能的调试方法
2. 检查 div 平衡 → 发现页面 div 缺少闭合标签
3. 修复 div 后检查脚本块 → 发现多余的 `}`
**根本原因**:`submitNewException()` 函数后有一个**多余的闭合括号 `}`**,导致 JavaScript 语法错误,整个脚本块无法执行
```javascript
// ❌ 错误:函数后有多余的 }
function submitNewException() {
// ...表单提交逻辑
}
//} <-- 多余的括号导致语法错误
// ✅ 正确:括号匹配
function submitNewException() {
// ...表单提交逻辑
}
```
**教训**:修改 HTML 时要确保 div 平衡;修改 JS 时要确保括号匹配。使用 Python 脚本做精确替换比手动编辑更安全。
---
### 经验3:按钮必须手动绑定 onclick
**问题现象**:新增弹窗后,弹窗中的按钮点击无反应
**根本原因**:新增的弹窗按钮**没有 onclick 属性**
**解决方案**:每个按钮都要显式绑定 onclick
```html
<!-- ❌ 错误:按钮没有 onclick -->
<button class="btn btn-primary">确定</button>
<!-- ✅ 正确:显式绑定 onclick -->
<button onclick="submitAddOrg()" class="btn btn-primary">确定</button>
```
**教训**:复制弹窗模板时,别忘了修改按钮的 onclick 属性。
---
### 经验4:按钮样式统一使用 CSS 类
**问题现象**:各页面按钮样式不统一,有内联样式如 `style="padding:10px 24px;"`
**解决方案**:统一使用 CSS 类
```html
<!-- ✅ 主按钮:黑色背景,胶囊形状 -->
<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 脚本做精确文本替换
```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%等)
**库存报表增强**:
- 新增滞销库存预警(30天以上)
- 新增安全库存预警
---
### 经验8:复合条件定位按钮
**问题现象**:页面有多个相似按钮(如"新增"),用 `document.querySelector` 定位困难
**解决方案**:使用复合条件或更精确的选择器
```javascript
// ❌ 困难:页面有多个 btn-primary
document.querySelector('.btn.btn-primary')
// ✅ 更好:使用 onclick 属性定位
document.querySelector('button[onclick="openAddModuleModal()"]')
// ✅ 更好:在页面内部查找
document.querySelector('#page-system_module button[onclick="openAddModuleModal()"]')
```
---
### 经验9:修改后立即验证
**原则**:每次修改后立即验证,不要积累多个问题
**验证清单**:
```bash
# 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 没有问题**:
1. **Compaction 只压缩对话历史**:不影响 HTML 文件本身
2. **不是日志文件**:不会无限增长
3. **每次修改同一个文件**:不是追加模式
4. **3700+ divs 仍然高效**:文件体积适中,渲染快
**什么时候该拆分成多文件**:
- 单个文件超过 2MB
- 需要多人协作(git merge 冲突)
- 模块之间完全独立
don't have the plugin yet? install it then click "run inline in claude" again.