返回 Skill 列表
extension
分类: 其它无需 API Key

原型设计技能

通用页面原型设计技能。基于58种设计风格(默认Figma风格),可开发复杂业务系统的HTML原型,涵盖标准页面结构、统计卡片、筛选条件、数据表格、弹窗设计。适用场景:①新建管理页面原型;②对照业务文档实现功能模块;③设计弹窗与详情页;④构建看板与列表视图页面。

person作者: contsunhubclawhub

原型设计技能

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/ 目录。

使用步骤

  1. 确定设计风格:根据项目需求选择设计系统

    • 管理后台常用:Figma、Linear、Notion、Vercel
    • 电商/消费:Airbnb、Spotify、Stripe
    • 企业级:IBM、Salesforce
  2. 读取对应 DESIGN.md

    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 | 紫色渐变、高级感 |

标准页面结构

<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 | 560px90vw | 固定 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

看板视图

<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

测试检查清单

  • [ ] 页面正常加载
  • [ ] 弹窗能打开
  • [ ] 弹窗内容正确
  • [ ] 弹窗能关闭
  • [ ] Tab 切换正常(如有)
  • [ ] 按钮点击有效

🔴 第四步:提交代码

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> 位置

解决方案

// 确保 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:表格横向滚动侵入左侧菜单

症状:表格内容多时,整个页面宽度被撑开,侵入左侧菜单栏

根本原因

  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 定义列宽

代码模板

<!-- 外层容器 -->
<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 内部

问题现象:"新增组织"按钮点击无反应,弹窗无法打开

排查过程

  1. 检查按钮 onclick 处理函数 → 存在
  2. 检查 JavaScript 函数 → 存在
  3. 检查弹窗 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 语法错误会导致整个脚本块失效

问题现象:修复弹窗位置后,按钮仍然无法点击

排查过程

  1. 使用 prototype-design 技能的调试方法
  2. 检查 div 平衡 → 发现页面 div 缺少闭合标签
  3. 修复 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%等)

库存报表增强

  • 新增滞销库存预警(30天以上)
  • 新增安全库存预警

经验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 没有问题

  1. Compaction 只压缩对话历史:不影响 HTML 文件本身
  2. 不是日志文件:不会无限增长
  3. 每次修改同一个文件:不是追加模式
  4. 3700+ divs 仍然高效:文件体积适中,渲染快

什么时候该拆分成多文件

  • 单个文件超过 2MB
  • 需要多人协作(git merge 冲突)
  • 模块之间完全独立