Files
ulthon_admin/.agents/skills/ulthon-admin-table/SKILL.md

820 lines
40 KiB
Markdown
Raw 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.

---
name: "ulthon-admin-table"
description: "后台列表页表格机制与定制完整指南:核心机制(ua 命名空间、三件套)、字段配置、内置渲染器、按钮系统(含页面分类标准 A/B/C/D)、搜索系统(前端配置 + 后端 buildTableParames)、后端配套、常见模式与排错、多形态渲染(受控模式 controlled、layoutList 五级模板协议 tplFunction/tplLay/tplVue/tplLayId/tplVueId、卡片形态、渲染上下文)。需要业务定制 CURD 生成的列表页(加按钮、改字段、调搜索)、排查按钮/搜索不生效、或自定义卡片/模板形态时调用。"
---
# 后台表格机制与定制(ulthon-admin-table)
本技能是后台列表页(基于 layui table 的 `ua.table.render`)完整指南。涵盖核心机制、字段配置、按钮系统、搜索机制、后端配套,以及实战中的常见模式与排错。
CURD 生成器生成列表页骨架后,所有业务定制(按钮、字段渲染、搜索)都在本技能范畴内。生成流程本身见 [ulthon-scheme-curd-workflow](./ulthon-scheme-curd-workflow/SKILL.md)。
## 何时调用
- 业务定制 CURD 生成的列表页(加/改字段、加按钮、调搜索)
- 排查"按钮不显示"、"搜索不生效"、"字段渲染不对"
- 自定义字段渲染(图片、状态、关联数据)
- 实现状态流转按钮(带 `_if` 显隐)
- 跨控制器跳转按钮
- 列表自定义卡片/模板形态(`controlled` + `layoutList`,见第十一章)
## 一、核心机制总览
### 1.1 `ua` 命名空间
`ua` = ulthon-admin 的 JS 全局对象,定义在 `public/static/plugs/ulthon-admin/`:
| 文件 | 作用 |
|------|------|
| `admin-table.js` | 表格核心:render / formatCols / renderOperat / 内置渲染器 / 搜索监听 / 受控模式接线与多形态渲染(reloadData shim、五级模板协议、渲染上下文) |
| `admin-table-store.js` | 受控模式数据层(全局 `window.UA_TABLE_STORE`):dataProvider / pager / selection / reload 四模块 |
| `admin-core.js` | ua 全局组装、headers、url、parame 等工具 |
| `admin-utils.js` | `ua.getDataBrage`、`ua.checkMobile`、`ua.getPageSetting` 等工具 |
| `admin-listen.js` | 事件监听(`ua.listen`) |
| `admin-api.js` | AJAX 请求封装(`ua.request`) |
| `ulthon-admin.js` | 入口(把 UA_ADMIN 挂到 window.ua) |
### 1.2 表格三件套
每个 CURD 模块的列表页由三个文件配合:
```
app/admin/view/<module>/
├── index.html # 极简 HTML,仅 table 元素 + data-auth-* 权限标记
├── _common.js # init 对象(URL 配置等)
└── index.js # ua.table.render({init, cols, toolbar?})
```
**index.html**(典型示例,仅 11 行):
```html
<div class="layuimini-container">
<div class="layuimini-main">
<table id="currentTable" class="layui-table layui-hide"
data-auth-index="{:auth('system.timer_config/index')}"
data-auth-edit="{:auth('system.timer_config/edit')}"
data-auth-export="{:auth('system.timer_config/export')}"
data-auth-modify="{:auth('system.timer_config/modify')}"
lay-filter="currentTable">
</table>
</div>
</div>
```
**`data-auth-X` 是按钮权限显隐的关键**:JS 里的 `auth: 'X'` 必须能在 HTML 找到对应的 `data-auth-X`,且值为 1 才显示。详见第六章。
### 1.3 前后端链路
```
[前端] index.js 的 cols 配置
│
├─ search: 'select'/'range'/false ──► [前端] renderSearch 自动生成搜索表单
│ │
│ ▼ 用户提交
│ [前端] listenTableSearch 构造 {filter, op} JSON
│ │
└─ templet: ua.table.xxx ──► [前端] tool/image/switch 渲染单元格
│
▼ GET /admin/<module>/index?filter={...}&op={...}
[后端] AdminControllerBase::buildTableParames()
│
├─ 解析 filter + op 为 WHERE 数组(按 op 操作符构造)
├─ relationSearch:关联表搜索时自动加 `table.field` 前缀
└─ 返回 [$page, $limit, $where, $excludes, $request_options, $group]
│
▼
[后端] 模型查询 → 分页数据 → JSON 返回 → 前端表格渲染
```
## 二、init 对象(`_common.js` 内容)
`init` 是表格的"配置中心",在 `_common.js` 中定义,被 `index.js` 引用。
**完整字段表**:
| 字段 | 类型 | 说明 |
|------|------|------|
| `tableElem` | string | 表格元素选择器,默认 `'#currentTable'` |
| `tableRenderId` | string | 表格实例 ID,用于 reload,默认 `'currentTableRenderId'` |
| `indexUrl` | string | 列表数据 URL(如 `'system.timer_config/index'`) |
| `addUrl` | string \| '' | 新增页 URL,空字符串则不显示新增按钮 |
| `editUrl` | string \| '' | 编辑页 URL |
| `readUrl` | string \| '' | 详情页 URL |
| `deleteUrl` | string \| '' | 删除接口 URL |
| `exportUrl` | string \| '' | 导出接口 URL |
| `modifyUrl` | string \| '' | 状态开关修改接口 URL(`switch` 渲染器自动调用) |
| `formFullScreen` | 'true' \| 'false' | 表单是否全屏(字符串) |
**典型示例**(`app/admin/view/system/timer_log/_common.js`):
```js
var init = {
tableElem: '#currentTable',
tableRenderId: 'currentTableRenderId',
indexUrl: 'system.timer_log/index',
addUrl: '',
editUrl: '',
readUrl: 'system.timer_log/read',
deleteUrl: '',
exportUrl: '',
modifyUrl: '',
};
```
只读列表(B 类页面,详见第六章)的所有写按钮 URL 留空字符串即可。
## 三、字段配置(cols 数组项)
`ua.table.render({ cols: [[...]] })` 的 `cols` 是二维数组(layui 标准),每个列项支持以下参数。
### 3.1 基础参数
| 参数 | 类型 | 说明 |
|------|------|------|
| `field` | string | 数据字段名;支持关联字段如 `appCodeBind.tube_code` |
| `title` | string | 表头文字 |
| `width` | number | 列宽(像素) |
| `minWidth` | number | 最小列宽 |
| `sort` | boolean | 是否允许排序 |
| `type` | string | 特殊列:`'checkbox'`(多选)/ `'radio'`(单选) |
| `fixed` | 'left' \| 'right' | 固定列 |
| `edit` | 'text' | 行内编辑(更新到 `modifyUrl`) |
| `hide` | boolean | 默认隐藏(用户可在列配置里开启) |
### 3.2 渲染参数
| 参数 | 类型 | 说明 |
|------|------|------|
| `templet` | function | 渲染函数,接收 `data` 返回 HTML。可用内置渲染器(见第五章)或自定义函数 |
| `defaultValue` | string | 数据为空时的默认显示值 |
### 3.3 搜索参数(详见第七章)
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `search` | boolean \| string | `true` | 搜索类型:`true`(文本)/ `false`(不搜索)/ `'select'`(下拉)/ `'number_limit'`(数值区间)/ `'time_limit'`(时间区间)/ `'range'`(范围) |
| `searchOp` | string | `'%*%'` | 搜索操作符,对应后端 WHERE 构造(见 7.1) |
| `selectList` | object | `{}` | 下拉选项;常用 `ua.getDataBrage('select_list_xxx')` 从页面字典读取 |
| `searchUrl` | string | - | 远程下拉数据源(如 `'system.timer_log/index?selectFields=task_name'`) |
| `searchValue` | string | - | 搜索框默认值 |
| `defaultSearchValue` | string | - | 默认搜索值(可从 URL query 自动回填) |
| `searchHide` | boolean | false | 是否默认隐藏该搜索项 |
| `fieldAlias` | string | = `field` | 搜索参数别名(用于关联表搜索时指定 `table.field`) |
| `searchTip` | string | `'请输入' + title` | 搜索框 placeholder |
| `timeType` | string | `'datetime'` | 时间搜索组件类型 |
> **注意**:`search` 默认是 `true`(每列默认参与搜索),要关闭必须显式 `search: false`。
## 四、内置渲染器(templet 取值)
通过 `templet: ua.table.xxx` 使用框架内置渲染器。
### 4.1 内置渲染器清单
| 渲染器 | 用途 | 关键参数 |
|--------|------|---------|
| `ua.table.image` | 单图/多图显示 | `imageWidth`(默认 200)/ `imageHeight`(默认 26)/ `imageSplit`(默认 `\|`)/ `imageJoin`(默认 `<br>`) |
| `ua.table.switch` | 开关组件(自动调 modify 接口) | `filter`(默认 = field)/ `checked`(默认 1,等于该值时勾选) |
| `ua.table.list` | 根据 selectList 映射显示文本 | `selectList` |
| `ua.table.date` | 日期格式化 | (从字段值自动格式化) |
| `ua.table.url` | 链接显示 | `urlNameField`(链接显示文字字段,可为 string 或 function) |
| `ua.table.filePreview` | 文件预览(图片或图标) | (根据 mime_type 自动判断) |
| `ua.table.tool` | **行操作按钮渲染器**(特殊,见第六章) | `operat`(按钮配置数组) |
### 4.2 使用示例
```js
// 图片字段(自动识别多图,按 | 分隔)
{field: 'head_img', title: '头像', search: false, templet: ua.table.image}
// 状态开关(自动调 modifyUrl,data 字段为 id,value 为 0/1)
{field: 'status', title: '状态', search: 'select',
selectList: {0: '禁用', 1: '启用'}, templet: ua.table.switch}
// 状态徽章(用 list 把 0/1 映射成文字)
{field: 'status', title: '状态', search: 'select',
selectList: {0: '禁用', 1: '启用'}, templet: ua.table.list}
// 时间字段
{field: 'create_time', title: '创建时间', search: 'range', templet: ua.table.date}
```
### 4.3 自定义 templet 函数
```js
{field: 'is_synced', title: '同步状态', width: 100, templet: function(d) {
if (d.is_synced == 1) {
return '<span class="layui-badge layui-bg-green">已同步</span>';
}
return '<span class="layui-badge">未同步</span>';
}}
```
自定义 templet 接收行数据 `d`,返回 HTML 字符串。
## 五、按钮系统
后台表格有两类按钮:**顶部工具栏**(toolbar)和**行级操作按钮**(operat)。
### 5.1 顶部工具栏(toolbar)
通过 `ua.table.render({ toolbar: [...] })` 配置。**不声明时默认 `['refresh', 'add', 'delete', 'export']`**——这意味着只读页面如果不覆盖,会显示无用的"新增/删除"按钮(见 9.2 常见错误)。
**字符串简写**(自动检查权限):
| 值 | 行为 | 权限节点 |
|----|------|---------|
| `'refresh'` | 刷新按钮(不带权限) | - |
| `'add'` | 新增按钮,打开 `init.addUrl` | `<module>/add` |
| `'delete'` | 批量删除(需配合 checkbox) | `<module>/delete` |
| `'export'` | 导出按钮,请求 `init.exportUrl` | `<module>/export` |
| `'selectConfirm'` | 选择器模式的"确定选择"按钮 | - |
**自定义按钮对象**:
```js
ua.table.render({
toolbar: [
'add', // 简写可与自定义混用
[{
text: '导入',
url: 'demo.index/import',
method: 'open', // 见 5.3 method 表
auth: 'import', // 对应 data-auth-import
class: 'layui-btn layui-btn-warm layui-btn-sm',
icon: 'fa fa-upload',
title: '导入数据',
extend: 'data-full="true"' // 额外 HTML 属性
}]
],
// ...
});
```
自定义按钮对象参数与行按钮通用,详见 5.4。
### 5.2 行级操作按钮(operat)
在 `cols` 最后一列配置 `templet: ua.table.tool` + `operat` 数组。**不声明 operat 时,tool 函数内自动填充 `['edit', 'delete']`**(admin-table.js:1055)。
```js
{
field: '',
title: '操作',
width: 250,
fixed: 'right',
templet: ua.table.tool,
operat: [
'edit', // 字符串简写
[{
text: '设置密码',
url: init.password_url, // 来自 _common.js
method: 'open',
auth: 'password',
class: 'layui-btn layui-btn-normal layui-btn-xs',
field: 'id', // URL 拼接的主键字段
titleField: 'username', // 弹窗标题用某字段值(如"设置密码 - admin")
}],
'delete'
]
}
```
**字符串简写**:
| 值 | 行为 | 默认 method | 权限节点 |
|----|------|------------|---------|
| `'edit'` | 编辑按钮 | `'tab'`(标签页打开) | `<module>/edit` |
| `'delete'` | 删除按钮(带确认) | `'get'` | `<module>/delete` |
### 5.3 method 方法表
| method | 行为 | 适用场景 |
|--------|------|----------|
| `'tab'` | 主内容区导航(layuimini-content-href) | 详情页、编辑页(全页面跳转) |
| `'open'` | 弹窗打开(layer.open iframe) | 添加、导入等弹窗表单 |
| `'request'` | AJAX GET 请求 + 自动 confirm | 危险/不可逆操作:解绑、撤销、封闭 |
| `'blank'` | 新标签页打开 | 外部链接、文件预览 |
| `'get'` | 直接 GET 请求(不带 confirm) | 内置 `'delete'` 用此方法 |
| `'none'` | 不绑定行为,配合 extend 自定义监听 | 自定义 JS 交互 |
### 5.4 自定义按钮对象完整参数表
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `text` | string | - | 按钮文字 |
| `title` | string | = `text` | 提示文字 / 弹窗标题 / `request` 方法的 confirm 文案 |
| `url` | string \| function | `''` | 请求 URL;为 function 时接收 `(data, operat)` 动态构造 |
| `method` | string | `'open'` | 见 5.3 method 表 |
| `field` | string \| function | `'id'` | URL 拼接的主键字段名;为 function 时可动态构造 URL |
| `auth` | string | `'add'` | 权限节点名(对应 HTML 的 `data-auth-X`) |
| `class` | string | `''` | HTML class(layui-btn 样式) |
| `icon` | string | `''` | 图标 class |
| `extend` | string | `''` | 额外 HTML 属性(如 `data-full="true"`) |
| `titleField` | string | - | 用数据中某字段值作为标题(如"编辑 - 张三") |
| `extra` | string | - | 标题附加字段(自动拼到 title 前) |
| `_if` | function \| string | 始终显示 | **显隐条件**:function 接收 `(data, operat)` 返回布尔;string 时按字段名取值 |
| `callback` | function | - | 点击回调(覆盖默认行为) |
| `checkbox` | boolean | false | 是否需要选中行 |
| `data` | string[] | `['id']` | 绑定到 HTML 的数据字段,生成 `data-{key}="{value}"` |
### 5.5 按钮颜色语义约定
| 颜色 | class | 语义 |
|------|-------|------|
| 灰色 | `layui-btn-primary` | 查看 / 中性操作 |
| 绿色 | `layui-btn-success` | 编辑 / 正向操作(内置 `'edit'` 默认) |
| 蓝色 | `layui-btn-normal` | 恢复 / 新增入口 |
| 橙色 | `layui-btn-warm` | 警告 / 可逆危险操作 |
| 红色 | `layui-btn-danger` | 危险 / 不可逆操作(内置 `'delete'` 默认) |
### 5.6 `_if` 条件显隐(最常用的进阶特性)
```js
operat: [
'edit',
[{
text: '触发',
url: init.triggerUrl,
method: 'request',
auth: 'trigger',
class: 'layui-btn layui-btn-warm layui-btn-xs',
// 仅 run_type='manual' 时显示
_if: function(data) {
return data.run_type === 'manual';
}
}]
]
```
`_if` 支持两种形式:
- **函数**:`function(data) { return boolean }`,data 是当前行数据
- **字符串**:传字段名,该字段为真才显示(如 `'_if': 'is_active'`)
未声明 `_if` 时默认恒真(总是显示)。**条件按钮缺少 `_if` 是常见错误**(见 9.2)。
### 5.7 跨控制器权限检查
按钮跳转到**其他控制器**的页面时(如 box_sn 页面的"绑定"按钮跳转 `code_bind/scan`),`auth` 的权限节点不在当前控制器下。需要在 HTML 用自定义 key 声明跨控制器的 `data-auth` 属性。
**HTML**:
```html
<table ... data-auth-bind="{:auth('app.code_bind/scan')}" ...>
```
**JS**:
```js
[{
class: 'layui-btn layui-btn-normal layui-btn-sm',
method: 'tab',
text: '绑定',
title: '扫码绑定',
auth: 'bind', // 对应 HTML 中的 data-auth-bind
url: 'app.code_bind/scan', // 跨控制器 URL
icon: 'fa fa-link'
}]
```
框架通过 `data-auth-{auth值}` 查找 HTML 属性做权限校验:有权限显示按钮,无权限自动隐藏。
### 5.8 operat 数组结构
`operat` 是一个数组,元素有两种形态,可混用:
```js
operat: [
[/* 按钮组1:每个对象是一个按钮 */],
[/* 按钮组2 */],
'edit', // 字符串简写
'delete'
]
```
每个 `[]` 内的对象是独立按钮,按顺序渲染。`renderOperat`(admin-table.js:769)做权限整列裁剪:只要任意一个按钮组内有按钮有权限,整列就保留。
## 六、页面分类标准(A/B/C/D)
根据数据的来源和维护方式,每个列表页必定属于以下某一类。**每个页面都应显式声明属于哪一类,并按对应标准配置 toolbar + operat。**
### A 类:完整 CRUD
数据由后台用户主动创建和维护,支持完整的增删改查。
```js
toolbar: [/* 自定义按钮(如有) */, 'add', 'delete', 'export'],
operat: [[/* 详情 */], [/* 业务操作(如有) */], 'edit', 'delete']
```
### B 类:只读列表
数据由系统流程生成或外部同步,后台仅查看,不支持增删改。
```js
toolbar: [],
defaultToolbar: ['filter'],
operat: [[/* 仅详情 */]]
```
### C 类:有限操作
数据由系统流程生成,不支持通用 add/edit/delete,但支持特定的业务操作(状态流转、撤销等)。
```js
toolbar: [[/* 特定入口按钮 */]/* , 'export'(如需) */],
operat: [[/* 详情 */], [/* 业务按钮(带 _if) */]]
// 不含 'edit' / 'delete',除非有明确的业务需求
```
### D 类:表单交互
非标准列表页,以表单/扫码交互为主,表格仅作为结果展示。
```js
toolbar: [],
// 通常无操作列,或仅详情
```
## 七、搜索系统
### 7.1 前端配置:searchOp 操作符表
`cols` 项的 `searchOp` 决定后端如何构造 WHERE:
| searchOp | 后端 WHERE 构造 | 适用场景 |
|----------|----------------|---------|
| `'%*%'`(默认) | `LIKE '%val%'` | 通用文本模糊搜索 |
| `'*%'` | `LIKE 'val%'` | 前缀匹配(如编号) |
| `'%*'` | `LIKE '%val'` | 后缀匹配 |
| `'='` | `= val` | 精确匹配(如状态) |
| `'in'` | `IN (val)` | 多值匹配 |
| `'min'` | `>= val` | 数值最小值 |
| `'max'` | `<= val` | 数值最大值 |
| `'min_date'` | `>= strtotime(val)` | 日期最小值(前端传字符串,后端转时间戳) |
| `'max_date'` | `<= strtotime(val)` | 日期最大值 |
| `'range'` | `>= strtotime(begin)` AND `<= strtotime(end)` | 时间范围(前端用 `search: 'range'` 自动生成起止输入) |
| `'none'` | 不进 where,作为 `request_options` | 业务自定义参数(不查库) |
> 默认操作符是 `%*%'`(LIKE 模糊匹配)。**搜索"不灵活"的错觉来源于不知道可以覆盖 `searchOp`**。
### 7.2 搜索类型与 searchOp 的关系
| `search` 值 | UI 类型 | 默认提交参数 | 常配 searchOp |
|------------|---------|-------------|---------------|
| `true`(默认) | 文本输入框 | `filter[field]=val` | `%*%` 或自定义 |
| `false` | 不显示搜索项 | - | - |
| `'select'` | 下拉选择 | `filter[field]=val` | `=` |
| `'number_limit'` | 数值区间(生成 `[field]min`/`[field]max`) | `filter[field]min=val&filter[field]max=val` | 自动用 `min`/`max` |
| `'time_limit'` | 时间区间 | 同上但格式为日期 | 自动用 `min_date`/`max_date` |
| `'range'` | 时间范围选择器 | `filter[field]=begin - end` | `range` |
### 7.3 select_list_* 字典通道(下拉数据来源)
下拉选项的标准通道,三处对齐:
1. **后端 assign**(控制器里):`$this->assign('select_list_{名}', $字典数组, true);`
- 第三个参数 `true` 表示同时压入 `data_brage` 通道
- 例:`$this->assign('select_list_status', StatusService::SN_STATUS_OPTIONS, true);`
2. **页面渲染**:所有 assign 变量被打包成 JSON,渲染进隐藏元素 `<script id="data-brage" type="text/plain">{$data_brage|raw|default='[]'}</script>`(位于 layout)
3. **前端读取**:`ua.getDataBrage('select_list_{名}')` 从 `#data-brage` 解析取值
```js
// cols 中引用:
{field: 'status', search: 'select', selectList: ua.getDataBrage('select_list_status'), ...}
```
> **口诀**:控制器 `assign` 的 key 必须等于 cols 里 `ua.getDataBrage('xxx')` 的参数,否则下拉为空。
### 7.4 后端处理:buildTableParames
`AdminControllerBase::buildTableParames()` 自动解析前端传入的 `filter` + `op` 参数为 WHERE 数组(`extend/base/common/controller/AdminControllerBase.php:405`):
```php
// 控制器 index 方法典型用法
public function index()
{
list($page, $limit, $where, $excludes, $request_options, $group) = $this->buildTableParames();
$list = $this->model
->where($where)
->page($page, $limit)
->select();
// ...
}
```
支持:
- **`$excludeFields`**:`buildTableParames(['field_to_exclude'])` 排除某些字段不进 where
- **`relationSearch`**:控制器属性 `$relationSearch = true` 时,自动给字段加 `table.field` 前缀
- **`$request_options`**:`op='none'` 的字段会进入此变量(业务自定义参数)
## 八、后端控制器配套
| 配套项 | 说明 |
|--------|------|
| `index()` | 使用 `buildTableParames()` 构造查询 |
| `$relationSearch` | bool 属性,开启关联表搜索(默认 false) |
| `$selectWhere` | array 属性,`selectList()` 方法的过滤条件 |
| `modify()` | 状态开关接口,`switch` 渲染器自动调用,接收 `id`/`field`/`value` |
| `selectList()` | 下拉数据源接口,接收 `selectFields` 参数 |
| `@NodeAnnotation` | 控制器方法注解,生成权限节点(与 `data-auth-X` 对应) |
**关联搜索示例**(`extend/base/admin/controller/mall/GoodsBase.php`):
```php
class GoodsBase extends AdminController
{
protected $relationSearch = true;
// 此时 cols 中 field: 'cate.title' 会自动转为 where: ['mall_goods.cate_id' => ...]
// 注意:关联字段需在模型里定义好 with 关系
}
```
## 九、常见模式与排错
### 9.1 常见模式速查(参考示例)
| 场景 | 参考文件 |
|------|---------|
| 标准列表(增删改查 + 状态开关) | `extend/base/admin/view/system/admin/index.js` |
| 行内排序(edit: 'text') | `extend/base/admin/view/system/admin/index.js`(sort 字段) |
| 多按钮(设置密码 + 编辑 + 删除) | `extend/base/admin/view/system/admin/index.js` |
| `_if` 控制按钮显隐 | `app/admin/view/system/timer_config/index.js`(仅 manual 显示触发按钮) |
| 详情按钮 + 状态徽章 | `app/admin/view/system/timer_config/index.js` |
| 关联表数据展示 | `extend/base/admin/view/mall/goods/index.js`(含 cate 信息) |
| 树形列表 | `extend/base/admin/view/system/menu/index.js` |
| 文件管理(图标预览) | `extend/base/admin/view/system/uploadfile/index.js` |
| 权限分配 UI(双表格) | `extend/base/admin/view/system/auth/assign_users.js` |
| 时间范围搜索 | `app/admin/view/system/timer_log/index.js`(start_time 用 `search: 'range'`) |
| 自定义 templet(多状态徽章) | `app/admin/view/system/timer_log/index.js`(status 字段) |
### 9.2 常见错误模式
**错误 1:operat 未声明 → CURD 默认 edit/delete**
```js
// 错误:依赖默认值
{ title: '操作', templet: ua.table.tool }
// tool 函数内自动填充 operat = ['edit', 'delete'],即使业务不需要
// 正确:显式声明,哪怕只保留详情
{ title: '操作', templet: ua.table.tool, operat: [[/* 详情 */]] }
```
**错误 2:toolbar 未声明 → 默认含 add/delete/export**
```js
// 错误:只读页面(B 类)不覆盖 toolbar
ua.table.render({ init: init, cols: [[...]] });
// 默认 toolbar = ['refresh', 'add', 'delete', 'export'],但 addUrl/deleteUrl 可能为空,按钮仍显示但点击报错
// 正确:只读页面显式清空
ua.table.render({ init: init, toolbar: [], defaultToolbar: ['filter'], cols: [[...]] });
```
**错误 3:条件按钮缺少 _if**
```js
// 错误:按钮始终显示,后端校验拒绝
{ text: '解绑', method: 'request', url: init.unbindUrl }
// 正确:前端条件过滤,减少无效点击
{ text: '解绑', method: 'request', url: init.unbindUrl,
_if: function (data) { return data.status == 可解绑状态值; } }
```
**错误 4:CURD 关联残留列**
CURD 生成时会将关联模型的全部字段(如 `systemAdmin.*`)输出为列。**每个页面应清理掉不需要展示的关联残留列**,只保留业务需要的字段。
**错误 5:select_list_* 不对应(下拉为空)**
控制器 `assign('select_list_xxx', ...)` 的 key 必须与 cols 里 `ua.getDataBrage('select_list_xxx')` 的参数完全一致(大小写敏感)。
**错误 6:权限节点不匹配(按钮不显示)**
- HTML 的 `data-auth-X` 必须存在
- JS 里的 `auth: 'X'` 必须能找到对应的 `data-auth-X`
- `{:auth('controller/action')}` 的节点路径必须与控制器 `@NodeAnnotation` 一致
排查工具:`php think admin:permission:nodes`(见 [ulthon-permission-cli](./ulthon-permission-cli/SKILL.md))
### 9.3 排错指南
**"按钮不显示"排查步骤**:
1. 检查 HTML 是否声明了对应的 `data-auth-X`(用浏览器开发者工具看 table 元素)
2. 检查 `{:auth('xxx/yyy')}` 的返回值是否为 1(用超管账号测试排除权限问题)
3. 检查 JS 里 `auth: 'X'` 的 X 与 HTML `data-auth-X` 是否完全一致(大小写)
4. 查看权限节点列表:`php think admin:permission:nodes`
**"搜索不生效"排查步骤**:
1. 浏览器抓 network 看请求是否带 `filter` 和 `op` 参数
2. 检查 `filter` JSON 是否包含期望的字段
3. 检查对应字段的 `op` 值是否符合预期(默认 `%*%`)
4. 后端用 `php think tools:db:query "SELECT ... WHERE ..."` 直接查表确认数据存在
5. 检查控制器是否调用了 `buildTableParames()` 而不是手动 `where()`
## 十、实测代码引用表
每个特性都标注实测来源(文件:行),若代码与本文档冲突,以代码为准。
| 特性 | 实测来源 |
|------|---------|
| `ua.table.render` 入口 | `public/static/plugs/ulthon-admin/admin-table.js:14` |
| `formatCols`(cols 加工) | `admin-table.js:921` |
| `renderOperat`(操作列整列裁剪) | `admin-table.js:769` |
| `renderToolbar`(顶部工具栏) | `admin-table.js:251` |
| `renderSearch`(搜索表单生成) | `admin-table.js:294` |
| `search` 默认 `true` | `admin-table.js:307`(`ua.parame(d.search, true)`) |
| `searchOp` 默认 `'%*%'` | `admin-table.js:312` |
| `search: 'number_limit'` / `'time_limit'` | `admin-table.js:348-369` |
| `listenTableSearch`(filter+op 构造) | `admin-table.js:1334` |
| `tool` 函数(自动填充默认 operat) | `admin-table.js:1053`,自动填充在 1055 |
| `tool` 内置 'edit' 处理 | `admin-table.js:1061` |
| `tool` 内置 'delete' 处理 | `admin-table.js:1082` |
| `operat._if` 显隐判定 | `admin-table.js:1113-1124` |
| `operat.auth` 默认 `'add'` | `admin-table.js:1105` |
| `list` 渲染器 | `admin-table.js:1169` |
| `filePreview` 渲染器 | `admin-table.js:1180` |
| `image` 渲染器(imageSplit/imageJoin) | `admin-table.js:1195-1213` |
| `url` 渲染器(urlNameField) | `admin-table.js:1215` |
| `switch` 渲染器(自动调 modify) | `admin-table.js:1245`,modify 调用在 `listenSwitch:1365` |
| `date` 渲染器 | `admin-table.js:1281` |
| `ua.getDataBrage` 实现 | `public/static/plugs/ulthon-admin/admin-utils.js:63-74` |
| `#data-brage` 隐藏元素 | `extend/base/admin/view/layout/default.html`(layout 文件) |
| `data_brage` assign | `extend/base/common/controller/AdminControllerBase.php:273` |
| `buildTableParames`(filter+op 解析) | `extend/base/common/controller/AdminControllerBase.php:405` |
| `$relationSearch` 属性 | `AdminControllerBase.php:69`(默认 false) |
| `$selectWhere` 属性 | `AdminControllerBase.php:63` |
| `selectList()` 方法 | `AdminControllerBase.php:489` |
| 实际示例:标准 CRUD | `extend/base/admin/view/system/admin/index.js` |
| 实际示例:`_if` 条件按钮 | `app/admin/view/system/timer_config/index.js` |
| 实际示例:自定义 templet | `app/admin/view/system/timer_log/index.js` |
| `controlled` 受控分支接线 | `admin-table.js:914`(注册 store、自建分页与模板容器、首屏取数) |
| reloadData shim(受控分流/透传) | `admin-table.js:21` |
| `ua.table.reloadData` / `ua.table.checkStatus` 分发入口 | `admin-table.js:373` / `:379` |
| store 五入口(register/has/reload/fetch/checkStatus) | `admin-table-store.js:531` |
| layoutList 规范化(字符串/对象条目) | `admin-table.js:69`(normalizeLayoutList) |
| 五级钩子优先级解析 | `admin-table.js:194`(resolveLayoutEngine) |
| 内置 card 模板(tplFunction) | `admin-table.js:112`(builtinCardTemplate) |
| 渲染上下文构建器 | `admin-table.js:394`(buildLayoutContext) |
| tplFunction/laytpl/Vue 调用约定注释 | `admin-table.js:466-508` |
| renderLayoutForm(模板管线入口) | `admin-table.js:511` |
| 统一行选中委托(data-table-id + data-id) | `admin-table.js:158` |
| getLayoutForm / setLayoutForm | `admin-table.js:670` / `:678` |
| 默认形态解析(layoutDefault/checkMobile/记忆) | `admin-table.js:943` |
| 视图切换按钮注入(条目 > 1) | `admin-table.js:732`,记忆写入 `:2025` |
| `ua.checkMobile` 判定 | `admin-utils.js:6`,实现 `public/static/common/js/app.js:63` |
| 实际示例:名片形态(Vue 模板) | `extend/base/admin/view/system/admin/index.html:19` + `index.js:10` |
## 十一、多形态渲染(受控模式与自定义模板)
列表除了引擎表格形态,还能以卡片等自定义模板形态渲染。模板形态运行在受控模式之上:数据由框架数据层接管,模板只负责"一行/一屏数据长什么样"。官方完整范例是用户管理的名片形态(`extend/base/admin/view/system/admin/index.html:19` 的 usercardTpl 节点 + `extend/base/admin/view/system/admin/index.js:10` 的 layoutList 配置)。
### 11.1 受控模式
`ua.table.render` 的 options 加 `controlled: true` 即开启。受控后页面数据由 `admin-table-store.js`(全局 `window.UA_TABLE_STORE`)接管:引擎不再自己发请求,首屏、搜索、排序、翻页、switch 回滚、删除刷新等所有数据重载统一走 reloadData 的 shim 分流进 store。
| 组件 | 说明 |
|------|------|
| shim(`table.reloadData` 包装) | 受控表(store 已注册该 id)→ `store.reload`;非受控表原样透传引擎(admin-table.js:21) |
| `ua.table.reloadData(id, opts)` | 页面代码刷新表格的推荐入口:按受控与否自动分流,受控/非受控页面通吃(admin-table.js:373) |
| `ua.table.checkStatus(id)` | 取选中行的推荐入口,同样自动分流(admin-table.js:379) |
旧页面里直呼 `layui.table.reloadData` / `layui.table.checkStatus` 的代码仍会被 shim 覆盖到,但新代码请统一用 `ua.table.reloadData` / `ua.table.checkStatus`。
store 内部四个模块(admin-table-store.js):
| 模块 | 职责 |
|------|------|
| `dataProvider` | 取数(fetch)与状态:where / sort / 当页数据 / count |
| `pager` | laypage 封装;受控表分页条由框架自建(`.ua-store-pager`),页面不用管 |
| `selection` | 选中集合:跨页保持勾选,与引擎勾选是同一事实源 |
| `reload` | 重载编排:合并 where/page 后 fetch,成功回调里按当前形态分发渲染 |
对外五个主入口 `register / has / reload / fetch / checkStatus`(admin-table-store.js:531)。业务页面一般不直接调 store,用上表两个 `ua.table.*` 分发入口就够了。
受控表在引擎视图与分页条之间有一个模板渲染容器 `#ua-layout-{tableId}`,模板形态的内容渲染在这里,与引擎数据区互斥显隐;工具栏与搜索区两形态共用(卡片形态下新增/批量删除/搜索照常可用)。
### 11.2 五级模板协议(layoutList)
`layoutList` 声明表格支持的形态清单,条目有两种写法:
```js
// 字符串条目:'table'/'card' 有内置实现(标题缺省「表格」/「卡片」),
// 其他字符串以自身为标题
layoutList: ['table', 'card']
// 对象条目:自定义形态,name 必填,title 供视图切换下拉显示,icon 可选
layoutList: ['table', {name: 'usercard', title: '名片', tplVueId: '#usercardTpl'}]
```
**模板钩子五级优先级**(按序取第一个命中的,提供一种即生效):
| 优先级 | 钩子 | 类型 | 引擎 |
|--------|------|------|------|
| 1 | `tplFunction` | function | 框架行循环,`(行上下文, 列表上下文)` 返回单行 HTML 字符串 |
| 2 | `tplLay` | string | laytpl 模板字符串,行级作用域 |
| 3 | `tplVue` | string | Vue 模板字符串,列表级单实例 |
| 4 | `tplLayId` | string | `<script type="text/html">` 节点选择器(laytpl) |
| 5 | `tplVueId` | string | `<script type="text/x-template">` 节点选择器(Vue) |
- 五种钩子全无且条目不是 `table`:console.warn(每条目仅一次)并回退表格形态
- 内置 `card` 本质是一个框架内置的 tplFunction 条目(header/body/footer 卡片骨架,样式类 `.ua-data-card` 族);条目自带任意钩子时完全覆盖内置实现
- 模板渲染失败(节点缺失、laytpl 错误、Vue 未引入或挂载失败)自动 console.error 并回退表格形态,不白屏;回退不写记忆,修复后可再切换
**默认形态与视图切换**:
- 默认 `table`。解析优先级:localStorage 按页记忆 > 显式 `layoutDefault` 或手机端自动 card > `table`(admin-table.js:943)
- `layoutList` 条目数 > 1 时,工具栏自动注入「视图」按钮;点击下拉条目 = 写记忆 + 切形态,切换用的是 store 当页数据,零网络请求
- 记忆键 `table-layout-default`(经 `ua.getPageSetting` 按页加前缀存储),运行期 API:`ua.table.getLayoutForm(id)` 读当前形态、`ua.table.setLayoutForm(id, name)` 切形态
**两条硬约束**:
1. 模板渲染容器恒带 `layui-form` 类与 lay-filter(框架自建容器已保证,页面无需关心)。表单控件的渲染美化与事件都要求 `.layui-form` 祖先,模板里写 switch/checkbox 依赖这条保证,缺失则全部失效。
2. laytpl/Vue 的模板节点(`<script type="text/html">` / `text/x-template`)**必须写在渲染容器之外**(页面根容器之后即可)。节点若写进容器内会被模板引擎编译时吞掉。这是使用者必须遵守的约束:框架不生成也不搬运模板节点。
### 11.3 渲染上下文
自定义模板拿到的数据由 `ua.table.buildLayoutContext(tableId)` 构建(调试时可直接在控制台调用查看)。
**行上下文**(tplFunction 第一参 / laytpl 的 `d` / Vue 的 `v-for` 项):
| 字段 | 说明 |
|------|------|
| `...row` | 行原始字段全部可用(username、status、create_time 等) |
| `_cells` | 对象,field → 该列 templet 渲染后的 HTML。只有配了渲染器的列才有内容;纯文本列恒为空串,勿引用,直接用原始字段 |
| `_operatHtml` | 操作列渲染产物(编辑/删除等按钮 HTML,已按 data-auth-* 裁剪权限) |
| `_checked` | 该行是否选中(store 选中集合) |
| `_disabled` | 该行是否禁选(行数据 LAY_DISABLED) |
**列表上下文**(tplFunction 第二参):`{tableId, rows, page, count, checkedAll}`。laytpl 形态挂在 `d._list` 上;Vue 形态叫 `list`,因为 Vue 实例不代理 `_`/`$` 前缀的数据键,裸写 `_list` 会 ReferenceError。
### 11.4 自定义形态示例
**laytpl 形态(tplLayId)**,简版:
```html
<!-- 页面模板区:渲染容器之外 -->
<script type="text/html" id="myCardTpl">
<div class="my-card">
<h3>{{- d._cells.title }}</h3>
<div>{{- d._operatHtml }}</div>
{{# if (d._checked) { }}<i class="layui-icon layui-icon-ok-circle"></i>{{# } }}
</div>
</script>
```
```js
layoutList: ['table', {name: 'mycard', title: '自定义卡', tplLayId: '#myCardTpl'}],
```
laytpl 语法实测(layui 2.10.1 内置 laytpl v2.0.0):`{{ d.x }}` 与 `{{= d.x }}` 都会 HTML 转义,`{{- d.x }}` 才是原样输出。`_cells`/`_operatHtml` 是渲染产物 HTML,必须用 `{{- }}`;纯文本用 `{{ }}` 转义更安全;`{{# }}` 写 JS 语句;列表上下文是 `d._list`。
**Vue 形态(tplVueId)**,简版:
```html
<!-- 页面模板区:渲染容器之外 -->
<script type="text/x-template" id="myVueTpl">
<div class="my-card" v-for="d in rows" :key="d.id"
:data-id="d.id" :data-table-id="list.tableId"
:class="{'ua-data-card-checked': d._checked}">
<h3 v-html="d._cells.title"></h3>
<div v-html="d._operatHtml"></div>
</div>
</script>
```
```js
layoutList: ['table', {name: 'mycard', title: '自定义卡', tplVueId: '#myVueTpl'}],
```
- `v-for="d in rows"` 承接行循环;`_cells`/`_operatHtml` 必须 `v-html` 输出,纯文本字段用 `{{ }}` 原生插值(自动转义);列表上下文是 `list`
- 行根元素带 `data-table-id` + `data-id` 两属性即获得统一行选中委托:点卡片切换勾选、选中态经 `:class` 绑 `ua-data-card-checked` 高亮回显;按钮/表单控件自动豁免不误触,零 JS
- 事件无需自己绑定:操作按钮(data-open/data-request 等属性)与 switch/checkbox 经页面级委托与 form 事件天然生效;`@click` 可用,但处理函数内不要修改模板数据(渲染上下文被冻结,数据更新一律整卡销毁重建)
**完整官方范例**:用户管理名片卡片,接入面只有两处,建议直接对照源码:
| 位置 | 内容 |
|------|------|
| `extend/base/admin/view/system/admin/index.html:19` | usercardTpl 模板节点 + 页面级网格样式 |
| `extend/base/admin/view/system/admin/index.js:10` | layoutList 一行接入(`controlled: true` 在第 5 行) |
该范例演示了三种典型取值通道:渲染器列(status)用 `_cells` + v-html 承接框架语义、纯文本列(username/create_time/phone)直接原始字段插值、头像(head_img)因名片需要圆形大图绕开渲染器直接绑原始字段。模板形态的网格布局由模板自带容器承担(Vue 形态不带内置卡片网格类)。
### 11.5 手机端默认行为
- 手机端判定 `ua.checkMobile()`(admin-utils.js:6,实现 public/static/common/js/app.js:63):UA 关键字(Android/iPhone/iPad 等)或 `window.screen` 宽 < 600 且高 < 800。判的是物理屏幕指标,**浏览器开发者工具仅改视口宽度不会触发**;设备仿真验证手机默认形态时,必须同时覆写 UA 或仿真 screen 尺寸,仅设 viewport 无效
- `layoutList` 含 `card` 条目时,手机端默认进入 card 形态;localStorage 记忆值优先级更高(记忆值不在 layoutList 内时忽略)
- 卡片形态能力边界:工具栏(刷新/新增/批量删除/导出)与搜索区两形态共用、照常可用;勾选语义由卡片勾位图标与选中委托承接(checkbox 列本身不进模板上下文);`_disabled` 行整元素不可勾选
## 相关技能
- [ulthon-scheme-curd-workflow](./ulthon-scheme-curd-workflow/SKILL.md):CURD 代码生成流程(生成本技能所定制的列表页骨架)
- [ulthon-scheme-definition](./ulthon-scheme-definition/SKILL.md):Scheme 定义(决定生成哪些字段、字段类型)
- [ulthon-page-api-dual-mode](./ulthon-page-api-dual-mode/SKILL.md):页面/接口同体机制(列表接口的 JSON 返回模式)
- [ulthon-permission-cli](./ulthon-permission-cli/SKILL.md):权限节点核对(`data-auth-X` 与 `@NodeAnnotation` 对不上时)
- [ulthon-tools-http-call](./ulthon-tools-http-call/SKILL.md):CLI 模拟请求验证列表接口