dataTable/README.md
2026-07-07 00:17:32 +08:00

444 lines
14 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.

# @apigo.cc/datatable
`@apigo.cc/datatable` 是建立在 `@apigo.cc/base` 上的同步数据表格组件,重点不是“能显示表格”,而是“能直接被页面首屏加载并完成渲染、筛选、编辑和保存”。
## 目录
- [先看依赖](#先看依赖) - 同步加载顺序
- [全局对象](#全局对象) - 字段类型注册入口
- [`<DataTable>`](#datatable) - 高性能表格组件
- [字段定义](#字段定义)
- [内置编辑能力](#内置编辑能力)
- [筛选、排序、固定列](#筛选排序固定列)
- [复制粘贴与批量编辑](#复制粘贴与批量编辑)
- [自定义字段类型](#自定义字段类型)
- [推荐示例](#推荐示例)
## 先看依赖
`datatable` 依赖 `state``bootstrap``base`,加载顺序仍然必须是同步的。
### 推荐加载顺序
```html
<script src="https://cdn.jsdelivr.net/npm/@apigo.cc/state@1.0.23/dist/state.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@apigo.cc/bootstrap@1.0.9/dist/bootstrap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@apigo.cc/base@1.0.23/dist/base.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@apigo.cc/datatable@1.0.16/dist/datatable.min.js"></script>
```
## 全局对象
### `DataTable`
`DataTable` 是本包挂到全局的对象。
```html
<script>
console.log(DataTable.getFieldTypes())
</script>
```
### 方法
| 方法 | 说明 |
|---|---|
| `registerFieldType(config)` | 注册字段类型 |
| `getFieldTypes()` | 获取当前所有已注册字段类型 |
## `<DataTable>`
`<DataTable>` 负责渲染、编辑、筛选、排序和虚拟滚动。内部会用到 `<AutoForm>`, `<Modal>`, `<Resizer>`,这些都来自 `base`
### 绑定方式
| 名称 | 写法示例 | 说明 |
|---|---|---|
| `editable` | `<DataTable editable>` | 开启编辑、删除、字段管理、保存按钮 |
| `$.state.fields` | `$.state.fields="window.fields"` | 字段定义数组 |
| `$.state.list` | `$.state.list="window.list"` | 数据行数组 |
### 事件
| 事件 | 触发时机 | `detail` |
|---|---|---|
| `save` | 点击底部保存按钮 | `{ list, fields }` |
| `savefields` | 字段新增 / 编辑 / 删除后 | `fields` |
| `remove` | 删除行后 | `{ items }` |
### 常用状态
| 名称 | 说明 |
|---|---|
| `editable` | 是否开启编辑模式 |
| `list` | 当前生效的数据列表,排序和筛选后会更新这里 |
| `fields` | 字段定义数组 |
| `_renderedList` | 当前 DOM 实际渲染的片段 |
| `prevHeight` | 虚拟滚动顶部占位高度 |
| `postHeight` | 虚拟滚动底部占位高度 |
| `_listStartIndex` | 当前渲染片段在原列表中的起始索引 |
| `selectedRowCount` | 当前选中行数 |
| `_originalList` | 原始数据快照,排序 / 筛选前的数据源 |
| `sortConfig` | `{ fieldId, direction }` |
| `filterConfig` | `{ [fieldId]: { mode, value, value2, selectedValues } }` |
| `activeFieldId` | 当前打开列菜单的字段 id |
| `activeField` | 当前打开列菜单的字段对象 |
| `activeModes` | 当前列允许的筛选模式 |
| `_columnStats` | 当前列高频值统计 |
| `_internalUpdate` | 内部同步标记,避免列表回写时触发二次筛选 |
| `_appliedHash` | 内部哈希缓存 |
| `_fieldsDirty` | 字段变更标记 |
| `_masterCellNodes` | 用于动态重建列模板的内部缓存 |
| `isDirty` | 是否存在未保存修改 |
| `isBulkEdit` | 是否处于批量编辑状态 |
### 方法
| 方法 | 写法示例 | 说明 |
|---|---|---|
| `addRow()` | `table.addRow()` | 新增一行空数据 |
| `deleteSelectedRow()` | `table.deleteSelectedRow()` | 删除当前选区对应的行 |
| `saveChanges()` | `table.saveChanges()` | 派发 `save` 事件 |
| `addField()` | `table.addField()` | 新增字段 |
| `editField()` | `table.editField()` | 编辑当前打开菜单的字段 |
| `deleteField()` | `table.deleteField()` | 删除当前打开菜单的字段 |
| `showColumnMenu(field, event)` | `table.showColumnMenu(field, event)` | 打开列菜单 |
| `hideColumnMenu()` | `table.hideColumnMenu()` | 关闭列菜单 |
| `editCell(row, field, cellNode)` | `table.editCell(row, field, cellNode)` | 打开单元格编辑器 |
| `onScroll()` | `table.onScroll()` | 滚动时刷新虚拟列表 |
### 推荐示例
```html
<script>
window.fields = [
{ id: 'id', name: 'ID', type: 'string', settings: { width: 70, formType: 'text', pinned: 'left' } },
{ id: 'name', name: 'Name', type: 'string', settings: { width: 160, formType: 'text', pinned: 'left' } },
{ id: 'role', name: 'Role', type: 'string', settings: { width: 120, formType: 'select', options: [{ label: 'Admin', value: 'admin' }, { label: 'Editor', value: 'editor' }] } },
{ id: 'active', name: 'Active', type: 'boolean', settings: { width: 90, formType: 'switch', labelOn: 'YES', labelOff: 'NO' } },
{ id: 'score', name: 'Score', type: 'number', settings: { width: 120, formType: 'number', prefix: '$', decimals: 2, thousandSep: true } },
{ id: 'tags', name: 'Tags', type: 'object', settings: { width: 180, formType: 'TagsInput' } },
{ id: 'created', name: 'Created', type: 'date', settings: { width: 140, formType: 'date' } },
{ id: 'actions', name: 'Actions', type: 'string', settings: { width: 120, formType: 'text', pinned: 'right' }, formatter: () => 'Edit' }
]
window.list = [
{ id: 1, name: 'Alice', role: 'admin', active: true, score: 12800.5, tags: ['Vue', 'AI'], created: '2026-06-01', actions: '' },
{ id: 2, name: 'Bob', role: 'editor', active: false, score: 980.25, tags: ['Docs'], created: '2026-06-02', actions: '' }
]
DataTable.registerFieldType({
value: 'TagsInput',
label: '{#Tags#}',
typeForDB: 'v4096',
schema: [{ name: 'placeholder', label: 'Placeholder', type: 'text' }],
formatter: val => Array.isArray(val) ? val.join(', ') : val
})
</script>
<div style="height: 600px" class="border rounded overflow-hidden bg-body">
<DataTable editable
$.state.fields="window.fields"
$.state.list="window.list"
$onsave="UI.toast('save: ' + JSON.stringify(event.detail.list))"
$onsavefields="UI.toast('fields changed')"
$onremove="UI.toast('removed: ' + event.detail.items.length)">
</DataTable>
</div>
```
## 字段定义
`fields` 是表格最重要的配置。每个字段都建议写完整,不要只写一个 `id` 就了事。
### 字段结构
| 字段 | 写法示例 | 说明 |
|---|---|---|
| `id` | `id: 'name'` | 列唯一标识,同时也是数据行中的字段名 |
| `name` | `name: 'Name'` | 表头显示名 |
| `type` | `type: 'string'` | 列的原始类型 |
| `memo` | `memo: 'User name'` | 字段备注,字段编辑器可见 |
| `isIndex` | `isIndex: true` | 是否索引字段 |
| `settings` | `settings: { width: 160, formType: 'text' }` | 编辑与展示配置 |
| `formatter` | `formatter: v => String(v)` | 自定义显示格式 |
### `settings` 字段
| 字段 | 写法示例 | 说明 |
|---|---|---|
| `width` | `width: 160` | 列宽,默认 `150` |
| `pinned` | `pinned: 'left'` | 固定列,`left``right` |
| `formType` | `formType: 'select'` | 编辑器类型 |
| `options` | `options: [{ label: 'A', value: 'a' }]` | 选择类选项 |
| `decimals` | `decimals: 2` | 数字小数位 |
| `prefix` | `prefix: '$'` | 数字前缀 |
| `suffix` | `suffix: '%'` | 数字后缀 |
| `thousandSep` | `thousandSep: true` | 数字千分位 |
| `labelOn` | `labelOn: 'YES'` | `switch` 为真时显示 |
| `labelOff` | `labelOff: 'NO'` | `switch` 为假时显示 |
| `format` | `format: 'YYYY-MM-DD'` | 日期时间格式说明 |
| `placeholder` | `placeholder: 'Enter value'` | 文本占位符 |
### 字段示例
```js
{
id: 'score',
name: 'Score',
type: 'number',
settings: {
width: 120,
formType: 'number',
decimals: 2,
prefix: '$',
thousandSep: true
},
formatter: value => value == null ? '' : String(value)
}
```
## 内置编辑能力
### 单元格编辑支持的 `formType`
`editCell()` 会把 `settings.formType || type || 'text'` 交给 `AutoForm`,因此这些类型都能直接编辑:
- `text`
- `number`
- `select`
- `checkbox`
- `radio`
- `switch`
- `date`
- `datetime`
- `textarea`
- `TagsInput`
- `DatePicker`
- `ColorPicker`
- `IconPicker`
### 字段编辑器默认可选类型
字段管理弹窗里,默认由 `DataTable.getFieldTypes()` 提供的类型是:
- `text`
- `number`
- `select`
- `checkbox`
- `switch`
- `datetime`
- `textarea`
### 重要区别
- `date``radio` 这些类型在单元格编辑里是可用的
- 但默认字段编辑器列表里不一定出现
- 如果你要让它们出现在字段管理弹窗里,直接用 `registerFieldType()` 补进去
### 默认字段类型的格式化规则
| 类型 | 显示规则 |
|---|---|
| `text` | 原样字符串化 |
| `number` | 支持 `decimals``prefix``suffix``thousandSep` |
| `select` | 显示匹配项的 `label` |
| `checkbox` | 把多选项 label 用逗号拼接 |
| `switch` | `true` 显示 `labelOn``Yes``false` 显示 `labelOff``No` |
| `datetime` | 默认字符串化 |
| `textarea` | 默认字符串化 |
## 筛选、排序、固定列
### 排序
- 点击表头右侧下拉按钮
- 选择 `ASC``DESC`
- 排序状态写在 `state.sortConfig`
### 筛选
列菜单支持三类筛选:
- 文本类:`contains`, `equals`, `starts`, `ends`
- 数字类:`=`, `>`, `<`, `between`
- 值列表:直接勾选统计出来的高频值
### 支持的列筛选模式
| `formType` | 可用模式 |
|---|---|
| `text` | `contains`, `equals`, `starts`, `ends` |
| `textarea` | `contains`, `equals`, `starts`, `ends` |
| `number` | `=`, `>`, `<`, `between` |
| `date` | `=`, `>`, `<`, `between` |
| `select` | `contains`, `equals` |
| `TagsInput` | `contains`, `equals`, `starts`, `ends` |
| `switch`, `checkbox`, `radio` | 选项勾选,不走文本筛选模式 |
### 固定列
字段上设置:
```js
settings: { pinned: 'left' }
```
或:
```js
settings: { pinned: 'right' }
```
就会固定在对应侧边。
## 复制粘贴与批量编辑
### 选择
- 鼠标拖选单元格区域
- `selectedRowCount` 会同步更新
- 选区会用于复制、粘贴和批量编辑
### 复制
- `Ctrl/Cmd + C` 把选区复制成 TSV
- 带引号、换行、制表符的值会自动转义
### 粘贴
- `Ctrl/Cmd + V` 会把剪贴板 TSV 回填到选区
- 单格选中时会把后续数据按行列铺开
### 批量编辑
- 在已选中的区域内双击一个单元格
- 如果选区跨多行,会进入批量编辑
- 保存时会把新值写入范围内每一行
## 自定义字段类型
### `DataTable.registerFieldType(config)`
```js
DataTable.registerFieldType({
value: 'MyTags',
label: '{#Tags#}',
typeForDB: 'v4096',
schema: [
{ name: 'placeholder', label: 'Placeholder', type: 'text' }
],
formatter: val => Array.isArray(val) ? val.join(' | ') : val
})
```
### `schema` 说明
`schema` 的格式和 `base` 里的 `AutoForm.schema` 一样,所以这里也能用:
- `name`
- `label`
- `type`
- `setting`
- `options`
- `if`
### 适合注册的场景
- 你要让字段编辑器认识一个新的 `formType`
- 你要给某种值定义专用 `formatter`
- 你要把扩展控件复用到表格内联编辑里
## 推荐示例
### 可编辑表格
```html
<script>
window.fields = [
{ id: 'id', name: 'ID', type: 'string', settings: { width: 70, formType: 'text', pinned: 'left' } },
{ id: 'name', name: 'Name', type: 'string', settings: { width: 160, formType: 'text', pinned: 'left' } },
{ id: 'role', name: 'Role', type: 'string', settings: { width: 120, formType: 'select', options: [{ label: 'Admin', value: 'admin' }, { label: 'Editor', value: 'editor' }] } },
{ id: 'active', name: 'Active', type: 'boolean', settings: { width: 90, formType: 'switch', labelOn: 'YES', labelOff: 'NO' } },
{ id: 'score', name: 'Score', type: 'number', settings: { width: 120, formType: 'number', prefix: '$', decimals: 2, thousandSep: true } },
{ id: 'created', name: 'Created', type: 'date', settings: { width: 140, formType: 'date' } }
]
window.list = [
{ id: 1, name: 'Alice', role: 'admin', active: true, score: 12800.5, created: '2026-06-01' },
{ id: 2, name: 'Bob', role: 'editor', active: false, score: 980.25, created: '2026-06-02' }
]
</script>
<div style="height: 560px" class="border rounded overflow-hidden">
<DataTable editable
$.state.fields="window.fields"
$.state.list="window.list"
$onsave="UI.toast('save')"
$onremove="UI.toast('remove: ' + event.detail.items.length)">
</DataTable>
</div>
```
### 只读大表 + 虚拟滚动
```html
<script>
window.fields = [
{ id: 'id', name: 'ID', type: 'string', settings: { width: 70, formType: 'text', pinned: 'left' } },
{ id: 'name', name: 'Name', type: 'string', settings: { width: 160, formType: 'text', pinned: 'left' } },
{ id: 'role', name: 'Role', type: 'string', settings: { width: 120, formType: 'select', options: [{ label: 'Admin', value: 'admin' }, { label: 'Editor', value: 'editor' }] } },
{ id: 'active', name: 'Active', type: 'boolean', settings: { width: 90, formType: 'switch', labelOn: 'YES', labelOff: 'NO' } },
{ id: 'score', name: 'Score', type: 'number', settings: { width: 120, formType: 'number', prefix: '$', decimals: 2, thousandSep: true } },
{ id: 'created', name: 'Created', type: 'date', settings: { width: 140, formType: 'date' } }
]
window.bigList = Array.from({ length: 2000 }, (_, i) => ({
id: i + 1,
name: 'User ' + (i + 1),
role: i % 2 ? 'admin' : 'editor',
active: i % 3 === 0,
score: Math.round(Math.random() * 10000) / 100,
created: '2026-06-01'
}))
</script>
<div style="height: 560px" class="border rounded overflow-hidden">
<DataTable
$.state.fields="window.fields"
$.state.list="window.bigList">
</DataTable>
</div>
```
### 自定义字段类型
```html
<script>
DataTable.registerFieldType({
value: 'TagsInput',
label: '{#Tags#}',
typeForDB: 'v4096',
schema: [{ name: 'placeholder', label: 'Placeholder', type: 'text' }],
formatter: val => Array.isArray(val) ? val.join(', ') : val
})
window.fields = [
{ id: 'tags', name: 'Tags', type: 'object', settings: { width: 180, formType: 'TagsInput' } }
]
window.list = [
{ id: 1, tags: ['Vue', 'AI'] }
]
</script>
<DataTable $.state.fields="window.fields" $.state.list="window.list"></DataTable>
```
### 备注
- 字段定义和数据列表建议放在 JS 里
- 外层容器最好明确给高度
- 需要保存时,监听 `save`;需要字段变更时,监听 `savefields`