bricks/aidocs/dataviewer.md
2025-10-05 06:39:58 +08:00

502 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# `bricks.DataViewer` 技术文档
> **版本1.0**
> **继承自:`bricks.VBox`**
> **用途:通用数据展示组件,支持分页加载、滚动加载、工具栏操作与动态表单交互**
---
## 概述
`bricks.DataViewer` 是一个可扩展的数据视图组件,用于在 Web 界面中以可视化方式展示和管理结构化数据。它基于 `bricks.VBox` 容器构建,具备以下核心功能:
- 支持异步分页加载远程数据(通过 `PageDataLoader`
- 垂直滚动区域自动加载上一页/下一页
- 可配置的工具栏(支持增删改查等操作)
- 行选择与事件通知机制
- 内置编辑表单弹窗(新增、更新、克隆、删除)
- 高度可定制化(可通过子类重写关键方法实现自定义渲染)
该组件通常作为列表、表格或卡片式数据展示的基础容器使用。
---
## 组件结构
```js
var bricks = window.bricks || {};
bricks.DataViewer = class extends bricks.VBox { ... }
```
### 注册名称
```js
bricks.Factory.register('DataViewer', bricks.DataViewer);
```
可通过工厂创建:
```js
let viewer = bricks.Factory.build('DataViewer', opts);
```
---
## 构造函数
```js
constructor(opts)
```
### 参数说明
| 参数 | 类型 | 必需 | 描述 |
|------|------|------|------|
| `opts.data_url` | String | 是 | 数据请求 URL |
| `opts.data_params` | Object | 否 | 请求附加参数 |
| `opts.page_rows` | Number | 否 | 每页行数,默认由 `PageDataLoader` 控制 |
| `opts.data_method` | String | 否 | HTTP 方法(如 `'GET'`, `'POST'`),默认 `'GET'` |
| `opts.cache_limit` | Number | 否 | 缓存页面数量限制 |
| `opts.editable` | Object | 否 | 编辑配置对象(见下文) |
| `opts.toolbar` | Object | 否 | 工具栏自定义项 |
| `opts.row_options` | Object | 否 | 行级选项(字段、排除字段等) |
### 初始化行为
1. 设置默认布局样式:
- 宽高为 `100%`
- 溢出隐藏(`overflow: hidden`
2. 创建 `PageDataLoader` 实例用于数据加载
3. 初始化状态变量(选中行、加载锁、偏移量等)
4. 绑定事件:`row_check_changed`
5. 延迟调用 `build_all()` 进行 UI 构建
---
## 核心属性
| 属性 | 类型 | 描述 |
|------|------|------|
| `loader` | `PageDataLoader` | 负责数据分页加载 |
| `scrollpanel` | `VScrollPanel` | 主内容滚动容器 |
| `filler_widget` | `Filler` | 占位容器,容纳 `scrollpanel` |
| `toolbar_w` | `IconTextBar` | 工具栏组件 |
| `select_row` | Widget | 当前选中的记录行 widget |
| `active_item` | Widget | 当前激活项目(保留字段) |
| `loading` | Boolean | 是否正在加载数据 |
| `data_offset` | Number | 数据起始偏移位置(用于反向插入) |
| `old_params` | Object | 上次请求参数,防止重复加载 |
| `key_select_items` | Array | 支持键盘导航的选择项集合 |
| `check_changed_row` | Object | 最近一次变更的行数据 |
| `keyselectable` | Boolean | 是否允许键盘选择 |
---
## 生命周期方法
### `async build_all()`
主 UI 构建入口,按顺序执行以下步骤:
```js
await this.build_other();
this.scrollpanel.bind('min_threshold', this.load_previous_page.bind(this));
this.scrollpanel.bind('max_threshold', this.load_next_page.bind(this));
await this.render();
this.set_key_select_items();
```
#### 子构建方法:
| 方法 | 功能 |
|------|------|
| `build_title_widget()` | (预留)构建标题区 |
| `build_description_widget()` | (预留)构建描述区 |
| `build_toolbar_widget()` | 构建顶部工具栏 |
| `build_records_area()` | 创建滚动面板用于显示数据行 |
| `build_other()` | 子类可扩展的额外构建逻辑(空实现) |
---
### `async render(params)`
重新加载并渲染数据。
#### 参数
- `params`: 请求参数(合并到原始 `data_params`
#### 流程
1. 若参数未变化 → 返回
2. 使用 `loader.loadData(params)` 获取数据
3. 清空当前内容
4. 执行前置处理 `before_data_handle()`
5. 处理数据 `dataHandle(d)`
> ⚠️ 自动去重:若 `params === old_params` 则跳过。
---
### `async before_data_handle()`
钩子函数,在数据处理前调用。可用于预处理或状态清理。
> 默认为空,供子类覆盖。
---
### `async dataHandle(d)`
处理从 `loader` 返回的数据对象。
#### 输入格式示例
```json
{
"rows": [...],
"add_page": 1,
"delete_page": 2
}
```
#### 行为
- 调用 `renderPageData(rows, add_page)`
- 如果有 `delete_page`,调用 `delete_page(page_num)` 删除旧页
---
## 数据渲染相关
### `build_records_area()`
创建主数据显示区域:
```js
this.filler_widget = new bricks.Filler({});
this.add_widget(this.filler_widget);
this.scrollpanel = new bricks.VScrollPanel({});
this.filler_widget.add_widget(this.scrollpanel);
```
> 使用 `Filler + VScrollPanel` 结构确保布局适应。
---
### `async renderPageData(data, page)`
将一批数据渲染成可视行。
#### 参数
- `data`: 数组,每项是一个数据记录
- `page`: 页面编号
#### 特殊逻辑
- 如果不是最大页(历史页):数据逆序,并从前部插入(维护时间顺序)
- 否则:正常追加至末尾
内部循环调用 `build_row()`
---
### `async build_record_view(record)`
**抽象方法**:生成单条记录的 UI 组件。
#### 默认实现
```js
var w = new bricks.VBox({width: '100px', height:'100px'});
w.set_css('test_box');
return w;
```
> ✅ 必须由子类重写以实现具体展示样式(如表格行、卡片等)
---
### `async build_row(record, page, pos)`
将一条记录添加到 `scrollpanel` 中。
#### 参数
- `record`: 数据对象
- `page`: 所属页码
- `pos`: 插入位置null 表示末尾)
#### 步骤
1. 调用 `build_record_view(record)` 创建 widget
2. 设置属性 `data-page` 便于后续删除
3. 添加到 `scrollpanel` 指定位置
---
## 工具栏与用户交互
### `build_toolbar_widget()`
根据配置生成工具栏按钮。
#### 支持的操作(当 `editable` 存在时)
| 名称 | 图标 | 条件 | 提示 |
|------|------|------|------|
| `add` | `add_icon` 或默认图标 | 总是显示 | 新增记录 |
| `update` | `update_icon` | 需选中行 | 更新选中项 |
| `clone` | `clone_icon` | 需选中行 | 克隆选中项 |
| `delete` | `delete_icon` | 需选中行 | 删除选中项 |
> 图标路径通过 `bricks_resource()` 解析 SVG 资源。
此外,支持合并外部传入的 `toolbar.tools`
最终创建 `IconTextBar` 并绑定命令事件。
---
### `command_event_handle(event)`
处理工具栏点击事件。
#### 分发逻辑
| 命令名 | 行为 |
|--------|------|
| `add` | 调用 `add_record()` |
| `update` | 调用 `update_record(select_row)` |
| `clone` | 调用 `clone_record(select_row)` |
| `delete` | 调用 `delete_record(select_row)` |
| 其他 | 触发全局事件 `dispatch(name, data)` |
> 若操作需要选中行但无选中项,则提示错误。
---
## 编辑功能
### 字段控制
#### `get_edit_fields()`
提取可编辑字段,过滤掉 `editexclouded` 中指定的字段。
> 结果保存在 `this.fields` 数组中。
#### `get_hidefields()`
获取应隐藏提交的字段(来自 `data_params`),转换为 `{name, value, uitype: 'hide'}` 形式。
---
### 表单构建
| 方法 | 功能 |
|------|------|
| `build_add_form()` | 构建“新增”表单 |
| `build_update_form(data)` | 构建“更新”表单(带 id 隐藏域) |
| `build_clone_form(data)` | 构建“克隆”表单(不包含 id |
所有表单均基于 `bricks.Form`,并注入隐藏字段和编辑字段。
---
### 弹窗管理
#### `build_window(icon, title, form)`
创建通用弹窗(`PopupWindow`)封装表单。
##### 配置
- 居中定位 (`archor: "cc"`)
- 可移动、可缩放
- 尺寸:宽 90%,高 70%
- 绑定表单的 `cancel` 事件关闭窗口
---
### 编辑流程
#### `add_record()`
1. 创建新增表单
2. 弹出窗口
3. 监听 `submited` 事件 → 调用 `add_record_finish(win, event)`
#### `add_record_finish(f, event)`
1. 关闭窗口
2. 重新加载数据
3. 解析响应 JSON 并构建反馈组件(如消息提示)
#### `update_record()`
1. 获取当前选中行数据
2. 创建更新表单(含 `id` 隐藏域)
3. 弹窗并监听 `submited``update_record_finish()`
#### `update_record_finish(win, form, event)`
1. 调用 `renew_record_view(form, row)` 更新本地视图
2. 显示服务器返回结果组件
3. 关闭窗口
#### `clone_record()`
`add_record`,但初始值为原记录数据
#### `delete_record(row, record)`
弹出确认对话框(`Conform`),确认后调用 `delete_record_act()`
#### `delete_record_act()`
1. 发送 DELETE 请求POST with body
2. 接收响应并构建反馈组件
3. 若成功(返回 Message 类型),移除对应行并刷新
---
## 滚动加载机制
### `load_previous_page()`
加载前一页数据(向上滚动触底)
#### 流程
1. 检查是否已在加载 → 防抖
2. 显示 loading 指示器(`Running`
3. 调用 `loader.loadPreviousPage()`
4. 成功则调用 `dataHandle(d)`
5. 恢复滚动位置(按 `pos_rate`
6. 隐藏 loading
> 错误被捕获并打印 debug 日志。
---
### `load_next_page()`
加载下一页数据(向下滚动触底)
逻辑类似 `load_previous_page()`,但无需调整滚动位置。
---
## 辅助方法
### `set_key_select_items()`
设置支持键盘导航的元素集合(除去第一个 filler widget
用于后续方向键选择。
---
### `delete_page(page)`
批量删除属于某一页的所有 DOM 元素。
通过 `[data-page="X"]` 查询 selector 获取 widgets 并逐个移除。
---
### `record_check_changed(event)`
处理行内复选框变更事件。
转发事件为 `row_check_changed`,携带 `user_data`
---
### `renew_record_view(form, row)`
用表单最新值更新某行的 `user_data`
```js
row.user_data = { ...row.user_data, ...form._getValue() };
row.renew(data); // 视图刷新
```
> `renew()` 是 widget 的生命周期方法,需子类实现。
---
## 事件系统
| 事件名 | 触发时机 | 参数 |
|-------|---------|------|
| `row_check_changed` | 行内复选框改变 | `{user_data}` |
| `command` | 工具栏按钮点击 | `{name, selected_row}` |
| `submited` | 表单提交成功 | `{params: Response}` |
| `conformed` / `discard` | 删除确认框选择 | —— |
---
## 设计原则与扩展建议
### 可扩展点(推荐子类覆盖)
| 方法 | 用途 |
|------|------|
| `build_other()` | 添加自定义组件 |
| `build_title_widget()` | 自定义标题 |
| `build_record_view(record)` | 自定义行渲染模板 |
| `before_data_handle()` | 数据加载前准备 |
| `renew(record)` in row widget | 行内容更新逻辑 |
---
### 性能优化特性
- 数据缓存与懒加载
- 滚动阈值触发分页
- 请求去重(参数比对)
- 页面级删除释放内存
---
## 使用示例(伪代码)
```js
let viewer = new bricks.DataViewer({
data_url: '/api/users',
data_params: { dept_id: 101 },
page_rows: 20,
editable: {
add_icon: 'imgs/user_add.svg',
new_data_url: '/api/users/create',
update_data_url: '/api/users/update',
delete_data_url: '/api/users/delete'
},
row_options: {
fields: [
{ name: 'name', label: '姓名', uitype: 'text' },
{ name: 'age', label: '年龄', uitype: 'number' }
],
editexclouded: ['created_at']
}
});
// 自定义行渲染
viewer.build_record_view = function(record) {
let w = new bricks.HBox({ width: '100%', padding: 10 });
w.add_widget(new bricks.Label({ text: record.name }));
w.add_widget(new bricks.Label({ text: record.age }));
w.user_data = record;
return w;
};
```
---
## 调试信息
- `bricks.debug_obj = this.scrollpanel;` —— 方便调试滚动容器
- 所有关键操作均有 `bricks.debug()` 输出
- 支持 `bricks.show_error()` 提示用户错误
---
## 依赖组件
| 组件 | 作用 |
|------|------|
| `bricks.VBox` | 布局基类 |
| `bricks.VScrollPanel` | 滚动容器 |
| `bricks.PageDataLoader` | 分页数据加载器 |
| `bricks.IconTextBar` | 工具栏 |
| `bricks.Form` | 表单引擎 |
| `bricks.PopupWindow` | 弹窗容器 |
| `bricks.Conform` | 确认对话框 |
| `bricks.Running` | 加载指示器 |
| `bricks.HttpJson` | JSON 请求客户端 |
| `bricks.widgetBuild` | 动态组件构建 |
---
## 版本历史
| 版本 | 修改内容 |
|------|----------|
| 1.0 | 初始公开文档版本 |
---
> 📝 文档生成时间2025-04-05
> © 2025 Bricks Framework Team