Collect-UI 测试用例生成指南
⚠️ 重要:在生成任何测试用例之前,必须先阅读并遵循本指南的所有规则。
🔴 生成测试用例时的绝对禁止事项
禁止1:JSON 键名缺少引号
// ❌ 绝对禁止
{"rules": [{type: "email", "message": "邮箱"}]}
{"danger": true}
// ✅ 必须这样写
{"rules": [{"type": "email", "message": "邮箱"}]}
{"danger": true}
禁止2:React children 是对象而不是数组
// ❌ 绝对禁止
{"tag": "col", "children": {"tag": "div"}}
{"tag": "form-item", "children": {"tag": "input"}}
{"tag": "dropdown", "children": {"tag": "button"}}
// ✅ 必须这样写
{"tag": "col", "children": [{"tag": "div"}]}
{"tag": "form-item", "children": [{"tag": "input"}]}
{"tag": "dropdown", "children": [{"tag": "button"}]}
禁止3:模板变量混合文本未包裹
// ❌ 绝对禁止
{"children": "当前页: ${page}, 共 ${total} 条"}
// ✅ 必须这样写
{"children": "${'当前页: '+page+', 共: '+total+' 条'}"}
🟢 生成测试用例的标准流程
生成前检查
在编写任何 JSON 之前,必须确认:
1. 所有键都有双引号吗?
2. children 是数组吗?(除了纯文本)
3. 混合文本在 ${...} 内吗?
生成后验证
# 生成后必须立即运行 npx jsonlint demo/data/test/xxx-demo.json # 验证不通过不能提交
📋 生成测试用例自检清单
JSON 格式检查
- • 所有键都有双引号
- •
{type: "email"}→{"type": "email"} - •
{min: 6}→{"min": 6} - •
{danger}→{"danger": true}
- •
React children 检查
- •
children 是数组或字符串
- •
{"children": "文本"}✅ - •
{"children": [{"tag": "span"}]}✅ - •
{"children": {"tag": "span"}}❌
- •
- •
需要数组的场景
- •
col组件的 children - •
row组件的 children - •
form-item组件的 children - •
dropdown组件的 children - •任何包含组件对象的 children
- •
模板变量检查
- • 混合文本在 ${...} 内
- •
❌ "当前页: ${page}" - •
✅ "${'当前页: '+page}"
- •
🚫 常见错误模式(必须避免)
错误1:键名缺少引号
// ❌
{"rules": [{type: "email"}]}
{"children": {"tag": "button"}}
// ✅
{"rules": [{"type": "email"}]}
{"children": [{"tag": "button"}]}
错误2:children 是对象
// ❌
{"tag": "form-item", "children": {"tag": "input"}}
// ✅
{"tag": "form-item", "children": [{"tag": "input"}]}
错误3:模板变量混合文本
// ❌
{"children": "当前页: ${page}"}
// ✅
{"children": "${'当前页: '+page}"}
✅ 正确示例集合
示例1:完整表单测试用例
{
"tag": "app",
"children": [
{
"tag": "layout-fit",
"initStore": {},
"children": [
{
"tag": "div",
"style": {"padding": "20px"},
"children": [
{"tag": "h3", "children": "表单测试"},
{
"tag": "form",
"name": "testForm",
"children": [
{
"tag": "form-item",
"name": "username",
"label": "用户名",
"rules": [{"required": true, "message": "请输入用户名"}],
"children": [{"tag": "input", "placeholder": "请输入用户名"}]
}
]
}
]
}
]
}
]
}
示例2:完整布局测试用例
{
"tag": "layout-fit",
"initStore": {},
"children": [
{
"tag": "div",
"style": {"padding": "20px"},
"children": [
{
"tag": "row",
"gutter": 16,
"children": [
{"tag": "col", "span": 12, "children": [{"tag": "div", "style": {"background": "#0092ff", "padding": "24px", "color": "#fff"}, "children": "col-12"}]},
{"tag": "col", "span": 12, "children": [{"tag": "div", "style": {"background": "#0092ff", "padding": "24px", "color": "#fff"}, "children": "col-12"}]}
]
}
]
}
]
}
🔄 快速检查三步法
生成 JSON 时快速检查:
1. 看所有键 → 有双引号吗?
2. 看所有 children → 是数组或字符串吗?
3. 看所有模板变量 → 混合文本在 ${...} 内吗?
完成后 → 运行 npx jsonlint 验证
🎯 测试用例质量标准
生成的测试用例必须满足:
- •✅ JSON 格式验证通过
- •✅ 没有 React children 错误
- •✅ 没有模板变量错误
- •✅ 浏览器控制台无错误
不满足以上标准的测试用例不能提交!
📚 参考资源
- •
AGENTS.md- 项目开发指南 - •
demo/data/- 正确示例集合 - •
npx jsonlint <file>- JSON 验证
Tabs 组件最佳实践
问题:Tabs 内容高度为 0 或显示不全,标签页切换不明显 分析:
- •上层封装 (
src/components/tabs/tabs.tsx): 支持activeKey自动绑定状态,无需手动设置 action - •底层 API (Ant Design Tabs):
type="line"提供更好的视觉反馈,flex布局确保内容填充高度
修复方案:
{
"tag": "tabs",
"activeKey": "${activeTab}",
"type": "line",
"size": "large",
"style": {
"flex": 1,
"display": "flex",
"flexDirection": "column",
"background": "#fff",
"padding": "20px",
"borderRadius": "8px",
"margin": "20px"
},
"items": [
{
"key": "tab1",
"label": "标签页 1",
"children": [
{
"tag": "div",
"children": "内容",
"style": {
"padding": "20px",
"background": "#fff",
"borderRadius": "6px",
"color": "#333",
"height": "100%"
}
}
]
}
]
}
要点:
- •使用
activeKey实现自动状态绑定 - •添加
flex样式确保高度填充 - •
type="line"比card提供更清晰的激活状态 - •内容区域使用白色背景和
height: "100%"实现全高显示 - •无需额外 action,组件自动处理状态更新
🔧 常见错误诊断与解决方案
错误1:keyField is required (listview)
症状:控制台报错 "listview 中 keyField is required",列表不显示
原因:listview 组件缺少必需的 keyField 属性
修复:
{
"tag": "listview",
"keyField": "id",
"itemData": "${dataList}",
"itemAttr": {...}
}
原则:
- •listview 必须设置
keyField指定数据唯一标识字段 - •如果数据没有唯一字段,组件会自动回退使用
id或生成 UUID - •推荐显式设置
keyField以提高性能和可预测性
错误2:JSON 解析失败
症状:[plugin:vite:json] Failed to parse JSON file, invalid JSON syntax
原因:JSON 格式错误,常见于:
- •缺少逗号或多余逗号
- •括号不匹配
- •缩进错误导致结构混乱
- •键名缺少引号(最常见!) 修复:
- •使用 JSON 验证工具检查:
npx jsonlint <file> - •在线验证:https://jsonlint.com/
- •IDE 插件验证(VS Code 自动检测)
错误2.1:JSON 键名缺少引号
症状:invalid JSON syntax found at position XXX
原因:JSON 中所有的键必须是带引号的字符串
错误示例:
// ❌ 错误:键名缺少引号
{
"rules": [
{type: "email", "message": "请输入邮箱"},
{len: 11, "message": "手机号必须是11位"},
{min: 6, "message": "密码至少6个字符"}
]
}
正确示例:
// ✅ 正确:所有键都有引号
{
"rules": [
{"type": "email", "message": "请输入邮箱"},
{"len": 11, "message": "手机号必须是11位"},
{"min": 6, "message": "密码至少6个字符"}
]
}
关键点:
- •JSON 中所有键都必须用双引号包围
- •JavaScript 对象字面量可以省略键的引号,但 JSON 不行
- •验证规则中的
type、len、min、max、pattern等都需要引号 - •编写完成后务必运行
npx jsonlint <file>验证
预防:
- •编写 JSON 后立即验证
- •参考
demo/data/中的示例配置 - •使用 VS Code 等支持 JSON 语法的编辑器
错误2.2:React children 不能是对象
症状:Objects are not valid as a React child (found: object with keys {tag, style, children})
原因:children 属性包含一个对象而不是数组或字符串
错误示例:
// ❌ 错误:children 是对象
{
"tag": "col",
"span": 6,
"children": {"tag": "div", "style": {...}, "children": "内容"}
}
正确示例:
// ✅ 正确:children 是数组
{
"tag": "col",
"span": 6,
"children": [{"tag": "div", "style": {...}, "children": "内容"}]
}
关键点:
- •React 的
children必须是字符串、数字、数组或 null - •不能直接传递组件对象作为 children
- •需要将组件对象包装在数组中:
[{"tag": "div", ...}] - •多层嵌套时每一层都需要是数组
常见场景:
- •
col组件的 children - •
row组件的 children - •任何需要嵌套组件的地方
预防:
- •使用 jsonlint 验证 JSON 格式
- •参考
demo/data/layout-demo.json中的正确用法 - •注意
children应该是[...]而不是{...}
错误3:数据嵌套层级过深
症状:itemData 或 rowData 无法正确渲染
原因:不必要的嵌套结构如 { "data": { "list": [...] } }
修复:
// ❌ 错误:多余嵌套
{
"initStore": {
"tableData": {
"list": [...]
}
},
"rowData": "${tableData.list}"
}
// ✅ 正确:扁平结构
{
"initStore": {
"dataList": [...]
},
"rowData": "${dataList}"
}
原则:
- •数据直接用数组,不要嵌套
- •参考
demo/data/demo.json中的数据结构 - •保持简单直接,减少引用路径
错误4:listview 属性名错误
症状:列表项不显示或渲染异常 原因:使用了错误的属性名 修复:
// ❌ 错误:使用 renderItem
{
"tag": "listview",
"renderItem": {...}
}
// ✅ 正确:使用 itemAttr
{
"tag": "listview",
"keyField": "id",
"itemData": "${dataList}",
"itemAttr": {...}
}
原则:
- •listview 使用
itemAttr定义项模板,不是renderItem - •table 使用
columnDefs定义列,不是columns - •参考
readme/components/listview.md文档
错误5:模板变量引用错误
症状:模板变量显示为 ${...} 原始字符串或 undefined
原因:变量名或引用格式错误
修复:
// ❌ 错误:使用 item
{
"itemAttr": {
"children": "${item.title}"
}
}
// ✅ 正确:使用 row
{
"itemAttr": {
"children": "${row.title}"
}
}
原则:
- •listview 项模板中使用
${row.field}访问数据 - •table 列中使用
${data.field}或${value} - •form 中使用
${form.getFieldValue('field')} - •参考对应组件文档中的变量说明
错误6:table API 空引用
症状:Cannot read properties of null (reading 'api')
原因:ag-grid API 在组件未初始化时就被访问
修复 (src/components/table/table.tsx):
// 添加空值检查
if (gridRef.current && gridRef.current.api) {
gridRef.current.api.resetColumnState();
gridRef.current.api.sizeColumnsToFit();
}
原则:
- •异步操作前检查引用有效性
- •resize 事件中使用延迟确保 DOM 就绪
错误7:Tabs 内容高度为 0
症状:标签页切换后内容区域空白 原因:缺少 flex 布局样式 修复:
{
"tag": "tabs",
"type": "line",
"style": {
"flex": 1,
"display": "flex",
"flexDirection": "column"
}
}
原则:
- •Tabs 组件需要 flex 容器确保内容填充
- •父容器也要设置高度
- •使用
type="line"比card视觉更清晰
错误8:不必要的 card 包裹
症状:布局过于嵌套,样式复杂 原因:所有组件都用 card 包裹 修复:
// ❌ 过度嵌套
{
"tag": "card",
"children": [{
"tag": "listview",
...
}]
}
// ✅ 简洁结构
{
"tag": "listview",
"className": "padding10",
...
}
原则:
- •简单展示不需要 card 包裹
- •使用
className: "padding10"提供间距 - •只在需要卡片视觉效果时使用 card
📋 开发检查清单
编写 JSON 配置前
- • 参考
demo/frontend/page_data/data/中的生产示例 - • 阅读
readme/components/[组件名].md文档 - • 确认组件必需属性(listview 需 keyField)
JSON 编写时
- • 扁平化数据结构,避免
xxx.list嵌套 - • 使用正确的属性名(itemAttr 不是 renderItem)
- • 使用正确的变量引用(row 不是 item)
完成后
- • 运行
npx jsonlint <file>验证 JSON 格式 - • 在浏览器中测试组件功能
- • 检查浏览器控制台错误
🧪 JSON 测试验证技能
技能:自动检测和修复 JSON 错误
使用场景:当 Vite 报 Failed to parse JSON file 错误时
操作步骤:
1. 读取 dev.log 获取错误信息 2. 定位错误文件和位置 3. 修复 JSON 语法错误 4. 运行 npx jsonlint <file> 验证
常见 JSON 错误速查表
| 错误类型 | 错误示例 | 正确写法 |
|---|---|---|
| 键名缺少引号 | {type: "email"} | {"type": "email"} |
| 键名缺少引号 | {danger} | {"danger": true} |
| 键名缺少引号 | {min: 6} | {"min": 6} |
| 括号不匹配 | ]}} | }]} |
| 逗号错误 | },] | }] |
| 多余逗号 | [1, 2, 3,] | [1, 2, 3] |
验证脚本
#!/bin/bash
# validate-all-tests.sh - 验证所有测试 JSON 文件
cd /data/react/collect-ui/demo/data/test
for file in *.json; do
echo "Checking $file..."
if npx jsonlint "$file" > /dev/null 2>&1; then
echo " ✅ $file - 验证通过"
else
echo " ❌ $file - 验证失败"
npx jsonlint "$file" 2>&1 | head -5
fi
done
从 dev.log 提取错误并修复
# 1. 查看 dev.log 中的错误 tail -50 /data/react/collect-ui/dev.log | grep "Failed to parse" # 2. 定位错误文件 # 错误信息格式: File: /path/to/file.json:行:列 # 3. 修复后验证 npx jsonlint /path/to/file.json
JSON 编写黄金法则
- •所有键必须用双引号 - JSON 不是 JavaScript 对象
- •所有字符串必须用双引号 - 包括
"true","false","null" - •数组/对象后无逗号 - 最后一个元素后不要加逗号
- •使用 jsonlint 验证 - 编写完成后立即运行验证
自动化测试 CI
# .github/workflows/validate-json.yml
name: Validate JSON
on: [push, pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install jsonlint
run: npm install -g jsonlint
- name: Validate all test files
run: |
for f in demo/data/test/*.json; do
jsonlint "$f" || exit 1
done
📚 参考资源
文档目录
- •
readme/components/- 所有组件详细文档 - •
demo/data/- 演示配置 - •
demo/frontend/page_data/data/- 生产配置示例 - •
AGENTS.md- 项目开发指南
常用组件文档
- •
readme/components/listview.md- 列表视图 - •
readme/components/table.md- 表格组件 - •
readme/components/tabs.md- 标签页 - •
readme/components/form.md- 表单
🎯 新增测试用例速查
基础组件测试
| 测试文件 | 组件 | 关键特性 |
|---|---|---|
breadcrumb-divider-demo.json | Breadcrumb, Divider | 面包屑导航、分割线样式 |
button-demo.json | Button | 按钮类型、危险按钮、图标按钮 |
icon-demo.json | Icon | 图标使用、动态图标 |
badge-demo.json | Badge | 徽章状态、计数显示 |
space-title-text-demo.json | Space, Title, Div, Span | 间距控制、标题层级、文本标签 |
pagination-step-dropdown-demo.json | Pagination, Step, Dropdown | 分页导航、步骤条、下拉菜单 |
表单组件测试
| 测试文件 | 组件 | 关键特性 |
|---|---|---|
input-demo.json | Input | 基本输入、密码框、文本域 |
input-full-demo.json | Input | 前置/后置图标、搜索框、字数统计 |
select-demo.json | Select | 下拉选择、单选/多选 |
select-full-demo.json | Select | 分组选项、搜索过滤、虚拟滚动 |
form-controls-demo.json | Checkbox, Radio, Switch | 表单控件基础 |
form-controls-advanced-demo.json | Checkbox, Radio, Switch | 复选框组、单选组、开关状态 |
date-progress-tag-demo.json | Date, Progress, Tag, Tooltip | 日期选择、进度条、标签、提示 |
cascader-demo.json | Cascader | 级联选择、动态加载 |
upload-demo.json | Upload | 文件上传、拖拽上传、图片预览 |
form-validation-demo.json | Form | 表单验证规则、动态验证 |
表格和列表组件测试
| 测试文件 | 组件 | 关键特性 |
|---|---|---|
table-demo.json | Table | 基本表格、列配置 |
table-full-demo.json | Table | 完整功能、分页、排序、筛选 |
listview-demo.json | ListView | 列表渲染、点击事件 |
tree-demo.json | Tree | 树形数据、展开/折叠 |
布局组件测试
| 测试文件 | 组件 | 关键特性 |
|---|---|---|
tabs-demo.json | Tabs | 基本标签页 |
tabs-full-demo.json | Tabs | 完整功能、图标、尺寸变化 |
card-demo.json | Card | 卡片容器、标题、操作按钮 |
layout-demo.json | Layout, Row, Col | 基础布局、栅格系统 |
layout-advanced-demo.json | Layout, Row, Col, Flex | 高级布局、响应式栅格 |
dialog-demo.json | Dialog | 对话框、确认框 |
Actions 测试
| 测试文件 | Action | 关键特性 |
|---|---|---|
actions-demo.json | message, confirm, dialog, drawer, ajax | 消息提示、确认框、弹窗、抽屉、HTTP请求 |
📝 测试用例编写技能
技能:创建新的测试用例
使用场景:为新组件或现有组件添加测试用例
操作步骤:
1. 在 demo/data/test/ 创建 [组件名]-demo.json 2. 使用 app 标签包装(需要 message 等功能时) 3. 使用 layout-fit 管理布局 4. 添加 initStore 定义测试数据 5. 验证 JSON 格式:npx jsonlint <file> 6. 在 App.tsx 注册测试用例
测试用例模板
{
"tag": "app",
"children": [
{
"tag": "layout-fit",
"initStore": {
"testData": []
},
"children": [
{
"tag": "div",
"style": {"padding": "20px"},
"children": [
{"tag": "h3", "style": {"marginBottom": "16px"}, "children": "组件名称"},
{"tag": "div", "style": {"marginBottom": "24px", "color": "#666"}, "children": "组件功能描述"},
{
"tag": "组件名",
"属性": "值",
"children": [...]
}
]
}
]
}
]
}
测试用例命名规范
| 类型 | 命名格式 | 示例 |
|---|---|---|
| 基础演示 | [组件名]-demo.json | button-demo.json |
| 完整功能 | [组件名]-full-demo.json | select-full-demo.json |
| 组合测试 | [功能名]-test.json | form-validation-demo.json |
测试用例注册流程
// 1. 在 demo/App.tsx 添加 import
import testXxxConfig from "./data/test/xxx-demo.json"
// 2. 在 testCases 数组注册
{
key: 'test/xxx-demo',
name: '组件名称',
description: '测试描述',
category: 'basic',
tags: ['组件', '标签']
}
// 3. 在 testConfigs 对象添加映射
'test/xxx-demo': testXxxConfig,
测试用例验证命令
# 验证单个文件 npx jsonlint demo/data/test/xxx-demo.json # 批量验证所有测试文件 cd demo/data/test for f in *.json; do npx jsonlint "$f"; done # 检查 dev.log 中的错误 tail -100 /data/react/collect-ui/dev.log | grep "Failed to parse"
测试用例质量检查清单
- • JSON 格式验证通过
- • 使用
app标签包装(需要 Actions 功能时) - • 使用
layout-fit管理布局 - •
initStore中包含必要的测试数据 - • 组件属性名称正确(参考组件文档)
- • 模板变量引用正确(使用
row而不是item) - • 在 App.tsx 正确注册
- • 浏览器中测试通过
- • 控制台无错误信息
常用组件测试模式
Input 组件测试
{
"tag": "input",
"placeholder": "请输入...",
"allowClear": true,
"disabled": false,
"maxLength": 100
}
Select 组件测试
{
"tag": "select",
"value": "${selectedValue}",
"options": [
{"value": "A", "label": "选项 A"},
{"value": "B", "label": "选项 B"}
],
"mode": "multiple"
}
Table 组件测试
{
"tag": "table",
"rowData": "${tableData}",
"columnDefs": [
{"field": "name", "headerName": "姓名"},
{"field": "age", "headerName": "年龄"}
],
"pagination": {"pageSize": 10}
}
ListView 组件测试
{
"tag": "listview",
"keyField": "id",
"itemData": "${listData}",
"itemAttr": {
"tag": "div",
"children": "${row.name}"
}
}
错误处理测试
测试消息提示
{
"tag": "button",
"children": "显示消息",
"action": [
{"tag": "message", "type": "success", "content": "操作成功"}
]
}
测试确认对话框
{
"tag": "button",
"danger": true,
"children": "删除",
"action": [
{
"tag": "confirm",
"title": "确认删除",
"content": "确定要删除吗?",
"onOk": [{"tag": "message", "type": "success", "content": "已删除"}]
}
]
}
性能测试建议
- •数据量测试:使用 100+ 条数据测试列表性能
- •分页测试:验证分页切换是否正常
- •虚拟滚动:大量数据时验证滚动性能
- •并发操作:测试快速点击/输入时的响应
测试用例目录结构
demo/data/test/ ├── hello-world.json # 入门示例 ├── button-demo.json # 按钮 ├── input-demo.json # 输入框 ├── select-demo.json # 选择器 ├── table-demo.json # 表格 ├── listview-demo.json # 列表 ├── tabs-demo.json # 标签页 ├── card-demo.json # 卡片 ├── form-validation-demo.json # 表单验证 ├── actions-demo.json # Actions └── ... # 更多测试
持续集成测试
# .github/workflows/test-validation.yml
name: Test Case Validation
on: [push, pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install jsonlint
run: npm install -g jsonlint
- name: Validate all test files
run: |
for f in demo/data/test/*.json; do
echo "Validating $f..."
jsonlint "$f" || { echo "Error in $f"; exit 1; }
done
- name: Check dev.log for errors
run: |
if grep -q "Failed to parse" dev.log 2>/dev/null; then
echo "JSON parse errors found in dev.log"
grep "Failed to parse" dev.log | head -5
exit 1
fi
echo "No JSON parse errors in dev.log"
🐛 调试经验总结
案例1:DatePicker 组件调试
问题症状
- •日期选择后输入框立即清空
- •但 store 中值已正确保存
- •独立使用和表单中使用表现不一致
调试过程
步骤1:添加组件日志
// 在组件入口添加
console.log('[DatePicker] props.value:', props.value)
console.log('[DatePicker] props.valueType:', typeof props.value)
// 在解析函数添加
function parseToDayjs(dateInput: any, format: string | undefined) {
console.log('[DatePicker] parseToDayjs input:', dateInput, 'type:', typeof dateInput)
// ...
}
步骤2:发现根本原因
[DatePicker] props.value: ${dateValue} ← 模板变量未被解析
[DatePicker] After transferProp, newProps.value: undefined ← transferProp 返回 undefined
步骤3:定位问题代码
// 问题代码:value 被解构,没有传给 transferProp
const { value, valueFormat, action, ...rest } = props
let newProps = transferProp(rest, "date") // value 不在 rest 中!
步骤4:修复方案
// 修复:手动解析 value 模板变量
const { value, valueFormat, action, store, namespace, ...rest } = props
const _target = genTarget(props)
let parsedValue = value
if (typeof value === 'string' && value.includes('${')) {
parsedValue = varValue(value, store, _target)
}
教训总结
- •
组件解构要谨慎
- •从 props 解构的属性不会传给 transferProp
- •如果属性包含模板变量,需要先手动解析
- •
日志是最好的朋友
- •从组件入口开始,逐步追踪值的变化
- •关注 props.value 的类型变化
- •
关注组件渲染时序
- •store 更新后组件重新渲染
- •新渲染时 props.value 应该已被解析
案例2:DateRangePicker 只能选一个日期
问题症状
- •日期范围选择器只能选择开始日期
- •选择第二个日期后弹窗关闭
调试过程
步骤1:检查 range 属性
{
"tag": "date",
"range": true // ✓ 属性已设置
}
步骤2:检查组件实现
// 问题代码:始终使用 DatePicker
return (
<DatePicker
{...newProps}
value={dayjsValue}
/>
)
步骤3:修复方案
// 根据 range 属性选择组件
const Component = range ? DatePicker.RangePicker : DatePicker
return (
<Component
locale={zhCN}
{...newProps}
value={dayjsValue}
/>
)
教训总结
- •
Ant Design 组件差异
- •DatePicker 单日期选择
- •RangePicker 范围选择
- •需要根据 mode/range 属性选择正确的组件
- •
数据格式差异
- •单日期:dateString 是字符串
- •范围选择:dateString 是数组
["2026-01-01", "2026-01-15"]
案例3:Form 组件高度异常
问题症状
- •form 组件占用满屏高度
- •inline 布局显示不紧凑
调试过程
步骤1:检查 DOM 结构
// 问题代码
<div onKeyDown={handleKeyDown} className="h100">
<Form {...rest}>
{props.children}
</Form>
</div>
步骤2:修复方案
// 移除 h100 class
<div onKeyDown={handleKeyDown}>
<Form {...rest}>
{props.children}
</Form>
</div>
教训总结
- •
CSS 类名影响布局
- •
h100设置 height: 100% - •导致组件撑满父容器
- •
- •
Form 组件使用建议
- •简单场景使用
layout: "inline" - •避免不必要的 wrapper div
- •简单场景使用
🔧 调试工具箱
常用调试命令
# 1. 查看 dev.log 错误 tail -100 /data/react/collect-ui/dev.log | grep "Failed to parse" # 2. 验证 JSON 格式 npx jsonlint demo/data/test/xxx.json # 3. 批量验证所有测试文件 cd demo/data/test for f in *.json; do npx jsonlint "$f"; done # 4. 查找特定错误 grep -r "Objects are not valid as a React child" . grep -r "Invalid Date" .
调试日志模板
// 组件入口日志
console.log('[ComponentName] ============ Render ============')
console.log('[ComponentName] props:', props)
// 解析函数日志
function parseValue(input: any) {
console.log('[ComponentName] parseValue input:', input, 'type:', typeof input)
// ... 处理逻辑
console.log('[ComponentName] parseValue result:', result)
return result
}
// 事件处理日志
const onChange = useCallback((value: any) => {
console.log('[ComponentName] onChange:', value)
// ... 处理逻辑
}, [])
常见问题快速定位
| 症状 | 可能原因 | 排查方向 |
|---|---|---|
模板变量显示为 ${...} | 变量未被解析 | 检查 transferProp 调用 |
| 组件值立即清空 | props 未正确传递 | 检查 props 解构 |
| 组件不显示 | JSON 格式错误 | 运行 jsonlint |
| React 渲染错误 | children 类型错误 | 检查 children 是数组 |
| Invalid Date | 日期格式问题 | 检查 valueFormat |
📚 参考资料
关键文件
- •
src/components/date/date.tsx- DatePicker 组件实现 - •
src/utils/transferProp.tsx- 属性传递与变量解析 - •
src/utils/varValue.tsx- 模板变量解析 - •
src/components/form/form.tsx- Form 组件实现
调试技巧
- •从组件入口开始,逐步追踪
- •关注 props.value 的变化
- •注意组件解构对 props 的影响
- •使用 console.log 添加关键节点日志
- •对比正常和异常场景的差异
案例4:Checkbox Group 和 Radio Group 不显示
问题症状
- •Checkbox Group 和 Radio Group 组件不显示
- •无报错但页面空白
调试过程
步骤1:检查控制台
- •无 React 错误
- •无组件渲染日志
步骤2:检查组件文件
src/components/checkbox/checkbox.tsx ← 存在 src/components/radio/radio.tsx ← 存在
步骤3:检查标签名称
// ❌ 错误:使用了不存在的标签
{"tag": "checkbox-group", ...}
{"tag": "radio-group", ...}
步骤4:修复方案
// ✅ 正确:使用组件名 + group 属性
{"tag": "checkbox", "group": true, ...}
{"tag": "radio", "group": true, ...}
教训总结
- •
组件标签规则
- •标签对应
src/components/[组件名]/[组件名].tsx - •
checkbox-group没有对应的组件文件 - •应该使用
checkbox并设置group: true
- •标签对应
- •
组件属性规则
- •Checkbox.Group =
checkbox+group: true - •Radio.Group =
radio+group: true
- •Checkbox.Group =
- •
常见组件映射
错误标签 正确标签 正确属性 checkbox-groupcheckboxgroup: trueradio-groupradiogroup: truedate-pickerdate- input-numberinputtype: "number"
案例5:Radio Group 和 Checkbox Group 只显示一个选项且无 label
问题症状
- •Radio Group 只显示一个选项
- •Checkbox Group 只显示一个选项
- •label 标签不显示
调试过程
步骤1:检查 JSON 配置
{
"tag": "radio",
"group": true,
"options": [
{"label": "男", "value": "male"},
{"label": "女", "value": "female"}
]
}
步骤2:检查组件实现
// 问题代码:直接传递 options 给 Group
if(isGroup){
return (
<Radio.Group {...newProps}>
{/* options 没有被渲染! */}
</Radio.Group>
)
}
步骤3:修复方案
// 正确:遍历 options 渲染 Radio 组件
if(isGroup){
return (
<Radio.Group {...newProps}>
{options && options.map((opt: any, index: number) => (
<Radio key={index} value={opt.value}>{opt.label}</Radio>
))}
</Radio.Group>
)
}
教训总结
- •
Ant Design Group 组件
- •Radio.Group 和 Checkbox.Group 需要显式渲染子组件
- •不能只传递
options属性,需要遍历渲染
- •
正确写法
json// Radio Group 正确写法 { "tag": "radio", "group": true, "options": [ {"label": "男", "value": "male"}, {"label": "女", "value": "female"} ] } - •
组件内部实现
- •需要遍历
options数组 - •为每个 option 创建 Radio/Checkbox 组件
- •设置
value和children(label)
- •需要遍历
🚨 Router 路由配置完整指南
核心问题:路由上下文
Router 路由系统依赖 React Router 的上下文机制。router 组件负责创建路由上下文,outlet、menu、router-tab 等组件需要在该上下文中才能正常工作。
症状:
[Outlet] useOutlet(): null outlet.tsx:71 [Outlet] current location: /collect-ui/simple-router.html
表示 outlet 组件无法获取路由上下文。
❌ 错误配置:router 和 outlet 同级
{
"tag": "app",
"children": [
{
"tag": "layout-fit",
"children": [
{
"tag": "menu",
"changeRouter": true,
"items": "${router}"
},
{
"tag": "router-tab"
},
{
"tag": "outlet"
},
{
"tag": "router",
"hash": true,
"data_home": "",
"router": "${router}"
}
]
}
]
}
错误原因:
- •
router组件与outlet同级 - •
outlet渲染时,React Router 上下文尚未建立 - •
useOutlet()返回null - •菜单点击无法触发路由跳转
症状:
- •
useOutlet()始终返回null - •菜单点击无反应
- •路由标签页不更新
- •页面内容不显示
✅ 正确配置:router 单独一级,页面配置独立加载
配置结构:
simple-router-framework.json (根配置)
└── layout-fit
├── router (单独一级,提供路由上下文)
└── children (页面配置,通过 data 加载)
└── menu + router-tab + outlet (在路由上下文中)
完整配置示例:
1. 根配置文件 (simple-router-framework.json)
{
"tag": "app",
"children": [
{
"tag": "layout-fit",
"initStore": {
"router": [
{
"path": "/",
"key": "first",
"label": "首页",
"name": "首页",
"redirect": "/base/a"
},
{
"path": "/base",
"key": "base",
"data": "demo/data/test/simple-page-base.json",
"label": "基础页面",
"children": [
{
"path": "/base/a",
"data": "demo/data/test/simple-page-a.json",
"key": "page-a",
"label": "页面 A"
},
{
"path": "/base/b",
"data": "demo/data/test/simple-page-b.json",
"key": "page-b",
"label": "页面 B"
}
]
}
]
},
"children": [
{
"tag": "router",
"hash": true,
"data_home": "",
"router": "${router}"
}
]
}
]
}
关键点:
- •
router组件在layout-fit.children中单独存在 - •
router提供路由上下文 - •页面配置通过
data字段加载
2. 页面配置文件 (simple-page-base.json)
{
"tag": "layout",
"style": {"minHeight": "100vh"},
"children": [
{
"tag": "sider",
"width": 200,
"theme": "dark",
"children": [
{
"tag": "div",
"style": {"padding": "16px", "color": "#fff", "fontWeight": "bold", "textAlign": "center"},
"children": "简单路由测试"
},
{
"tag": "menu",
"theme": "dark",
"mode": "inline",
"defaultOpenKeys": ["base"],
"changeRouter": true,
"rule": {
"key_field": "key",
"label_field": "label",
"to_field": "path"
},
"items": "${router}"
}
]
},
{
"tag": "layout",
"style": {"flex": 1, "display": "flex", "flexDirection": "column"},
"children": [
{
"tag": "router-tab"
},
{
"tag": "outlet"
}
]
}
]
}
关键点:
- •
menu设置changeRouter: true启用路由跳转 - •
rule定义菜单项与路由的映射关系 - •
router-tab和outlet必须在有路由上下文的组件中
3. 具体页面内容 (simple-page-a.json)
{
"tag": "div",
"style": {"padding": "20px"},
"children": "这是页面 A"
}
🚨 核心误区:router 配置与 framework 层是脱节的
这是最容易被忽略的误区!
很多同学只关注自己写的页面配置(simple-page-a.json),但没有理解 router 组件是如何把多层配置组合起来的。
路由加载原理
路由配置是分两层加载的:
┌─────────────────────────────────────────────────────────────┐ │ 第一层:framework 配置 (simple-router-framework.json) │ │ ├── initStore.router: 路由树结构(基础配置) │ │ └── data: 加载第二层配置 │ │ ↓ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ 第二层:页面布局配置 (simple-page-base.json) │ │ │ │ ├── menu + router-tab + outlet │ │ │ │ └── data: 加载第三层配置 │ │ │ │ ↓ │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ │ │ 第三层:具体页面内容 (simple-page-a.json) │ │ │ │ │ │ ├── tag: "div" │ │ │ │ │ │ └── children: "这是页面 A" │ │ │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘
关键点:
- •router 变量来源:根配置的
initStore.router定义路由树 - •框架层 data 加载:根配置的
data字段加载下一层配置 - •组合机制:
router组件会把initStore.router与data加载的配置合并 - •outlet 渲染:outlet 只会渲染当前路由对应的页面内容(第三层)
理解脱节现象
常见困惑:
"我在 simple-page-base.json 里写的 menu,items 用 ${router},
但是这个 router 变量是从哪里来的?"
答案:
- •
${router}引用的是根配置initStore.router - •虽然 menu 在第二层配置里,但
initStore是根配置传递下来的 - •router 组件创建上下文后,整个子树都能访问
store.router
路由配置组合规则
第一层(根配置):
{
"tag": "layout-fit",
"initStore": {
"router": [...] // 基础路由树
},
"children": [
{
"tag": "router",
"router": "${router}" // 使用 initStore.router
},
{
"tag": "页面组件",
"data": "第二层配置.json" // 加载第二层
}
]
}
第二层(页面布局):
{
"tag": "layout",
"children": [
{
"tag": "menu",
"items": "${router}" // 引用 initStore.router
},
{
"tag": "outlet" // 渲染当前路由页面
}
],
"data": "第三层配置.json" // 加载第三层(子页面)
}
第三层(具体页面):
{
"tag": "div",
"children": "页面内容"
}
从后台加载 router 的场景
当 router 从后台 API 加载时:
{
"tag": "layout-fit",
"initStore": {
"router": [] // 初始为空
},
"initAction": [
{
"tag": "ajax",
"api": "get:/api/menu/routes",
"adapt": {
"router": "${data}"
}
}
],
"children": [
{
"tag": "router",
"router": "${router}" // 从后台加载后才有值
},
{
"tag": "页面组件",
"data": "第二层配置.json"
}
]
}
关键理解:
- •后台返回的 router 配置会自动合并到
initStore.router - •menu 的
${router}能正确访问后台返回的路由树 - •这是框架自动处理的,不需要手动同步
误区总结
| 误区 | 正确理解 |
|---|---|
| menu 和 router 变量在同一个配置文件 | menu 在第二层,router 在第一层 initStore |
| 以为每个页面都需要定义 router | router 只在根配置的 initStore 中定义一次 |
| 以为 data 加载的页面会丢失 router 引用 | router 从 initStore 传递,不会丢失 |
| outlet 渲染整个页面布局 | outlet 只渲染当前路由对应的子页面内容 |
路由字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
path | string | 路由路径(如 /base/a) |
key | string | 唯一标识(用于菜单选中、router-tab) |
label | string | 显示名称(菜单显示、router-tab) |
data | string | 页面配置文件路径 |
redirect | string | 重定向路径 |
children | array | 子路由数组 |
Router 组件属性
| 属性 | 类型 | 说明 |
|---|---|---|
hash | boolean | 使用 hash 路由(#/a),默认 false |
basename | string | 基础路径(如 /collect-ui) |
data_home | string | 页面数据基础路径 |
router | array | 路由配置数组 |
Menu 组件路由配置
| 属性 | 类型 | 说明 |
|---|---|---|
changeRouter | boolean | 启用路由跳转 |
rule.key_field | string | 菜单项 key 字段名 |
rule.label_field | string | 菜单项标签字段名 |
rule.to_field | string | 菜单项跳转路径字段名 |
items | array | 菜单项数据 |
工作流程
三层加载机制:
┌─────────────────────────────────────────────────────────────────┐
│ 第一层:根配置加载 │
│ 1. 加载 simple-router-framework.json │
│ 2. 解析 initStore.router (路由树) │
│ 3. 创建 router 组件,建立路由上下文 │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 第二层:框架页面加载 │
│ 4. 访问 /base/a → router 匹配路由 │
│ 5. 加载 simple-page-base.json (data 字段) │
│ 6. 渲染 menu + router-tab + outlet (在路由上下文中) │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 第三层:具体页面内容渲染 │
│ 7. outlet 根据当前路由匹配 │
│ 8. 加载 simple-page-a.json (子路由的 data 字段) │
│ 9. 渲染页面内容 │
└─────────────────────────────────────────────────────────────────┘
详细步骤:
1. 加载根配置 simple-router-framework.json
├── initStore.router: 定义路由树结构
└── children.router: 创建路由上下文
↓
2. 访问 /base/a
├── 路由匹配:/base → 加载 simple-page-base.json
└── 路由匹配:/base/a → 加载 simple-page-a.json
↓
3. 渲染第二层 (simple-page-base.json)
├── menu 显示 ${router} (引用 initStore.router)
├── router-tab 显示标签页
└── outlet 准备渲染
↓
4. 渲染第三层 (simple-page-a.json)
└── outlet 渲染具体页面内容
文件结构
完整依赖关系:
simple-router.tsx
│
└── Render 组件加载
│
├── simple-router-framework.json (第一层)
│ │
│ ├── "tag": "app"
│ ├── initStore.router → 路由树定义 [1]
│ └── children[0].tag: "router" → 建立上下文 [2]
│ │
│ └── children[1].tag: "页面组件" (data 加载第二层)
│ │
│ └── simple-page-base.json (第二层)
│ │
│ ├── "tag": "layout"
│ ├── menu.items → ${router} [3]
│ ├── router-tab
│ ├── outlet → 渲染子页面 [4]
│ └── data → 加载第三层
│ │
│ └── simple-page-a.json (第三层)
│ │
│ └── "tag": "div", "children": "页面 A"
│
└── 组合渲染
├── router 上下文:覆盖整个应用
├── menu:从 initStore.router 读取路由树
├── outlet:渲染当前路由的 data 配置
└── router-tab:显示已打开的标签页
各层职责:
| 层级 | 文件 | 职责 |
|---|---|---|
| 第一层 | simple-router-framework.json | 定义路由树、创建 router 组件 |
| 第二层 | simple-page-base.json | 布局框架、菜单、outlet |
| 第三层 | simple-page-a.json | 具体页面内容 |
数据流向:
initStore.router (第一层)
│
├── menu.items=${router} (第二层)
│ └── 显示菜单项
│
└── router 组件配置
└── outlet 根据路由匹配加载第三层
常见错误排查
| 症状 | 可能原因 | 解决方案 |
|---|---|---|
useOutlet(): null | router 和 outlet 同级 | 将 router 单独一级 |
| 菜单点击无反应 | menu 缺少 changeRouter: true | 添加该属性 |
| 菜单不显示 | items 引用错误 | 检查 ${router} 路径 |
| 页面加载失败 | data 路径错误 | 检查 data_home 和 data 路径 |
| 路由不切换 | to_field 字段不匹配 | 确认 rule 配置 |
独立页面入口 (simple-router.tsx)
import React from 'react';
import ReactDOM from 'react-dom/client';
import {Render} from '../src/index';
import config from "./data/test/simple-router-framework.json"
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<Render {...config} />
</React.StrictMode>
);
访问地址:http://collect-ui.top:3000/collect-ui/simple-router.html#/base/a