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

792 lines
44 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:false 逃生口、layoutList 五级模板协议 tplFunction/tplLay/tplVue/tplLayId/tplVueId、双端自动注入、layoutDefault 按端默认与记忆链、卡片形态)。需要业务定制 CURD 生成的列表页(加按钮、改字段、调搜索)、排查按钮/搜索不生效、配置 layoutList/layoutDefault/视图切换,或判断页面是否需要 controlled:false 逃生口时调用;自定义卡片模板的创作(上下文契约、实战配方、设计模式、坑与验收)见 ulthon-card-template 技能。"
---
# 后台表格机制与定制(ulthon-admin-table)
本技能是后台列表页(基于 layui table 的 `ua.table.render`)完整指南。涵盖核心机制、字段配置、按钮系统、搜索机制、后端配套,以及实战中的常见模式与排错。
CURD 生成器生成列表页骨架后,所有业务定制(按钮、字段渲染、搜索)都在本技能范畴内。生成流程本身见 [ulthon-scheme-curd-workflow](./ulthon-scheme-curd-workflow/SKILL.md)。
## 何时调用
- 业务定制 CURD 生成的列表页(加/改字段、加按钮、调搜索)
- 排查"按钮不显示"、"搜索不生效"、"字段渲染不对"
- 自定义字段渲染(图片、状态、关联数据)
- 实现状态流转按钮(带 `_if` 显隐)
- 跨控制器跳转按钮
- 列表多形态机制与表格/卡片切换(`layoutList` + `layoutDefault`,见第十一章;自定义卡片模板的创作见 [ulthon-card-template](./ulthon-card-template/SKILL.md) 技能)
- 判断页面走的是哪种数据流,或是否需要 `controlled: false` 逃生口(见第十一章)
## 一、核心机制总览
### 1.1 `ua` 命名空间
`ua` = ulthon-admin 的 JS 全局对象,定义在 `public/static/plugs/ulthon-admin/`:
| 文件 | 作用 |
|------|------|
| `admin-table.js` | 表格核心:render / formatCols / renderOperat / 内置渲染器 / 搜索监听 / 受控数据流接线(默认数据流与 controlled:false 逃生口分流)与多形态渲染(reloadData shim、双端形态注入、五级模板协议、渲染上下文) |
| `admin-table-store.js` | 受控数据流数据层(全局 `window.UA_TABLE_STORE`,所有 `ua.table.render` 页面的默认数据层):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 # 纯结构零权限声明,权限由 layout 全局 UA_AUTH 注入
├── _common.js # init 对象(URL 配置等)
└── index.js # ua.table.render({init, cols, toolbar?})
```
**index.html**(典型示例):
```html
<div class="layuimini-container">
<div class="layuimini-main">
<table id="currentTable" class="layui-table layui-hide"
lay-filter="currentTable">
</table>
</div>
</div>
```
**权限判定协议**:index.html 不写任何权限声明。页面加载时 layout 注入全局载荷 `window.UA_AUTH = {all, controller, nodes}`(服务端一次性构建,超管 `all:true`),前端 `ua.auth(node)` 据此全局判定按钮显隐:短名(不含 `/`)自动补当前控制器前缀,跨控制器直接写全节点(如 `'app.code_bind/scan'`,与注册节点逐字一致,动作段大小写敏感)。详见 5.4 与 5.7。
> **升级注意**:本机制的判定实现位于 admin-core.js,其静态引用带框架版本号缓存键(下游部署为 site_version)。升级框架后若按钮显隐异常,先强制刷新浏览器缓存(Ctrl+F5)再排查,避免旧缓存 JS 与新页面叠加造成误判。
### 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', // 短名=当前控制器动作(等价 'demo.index/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']`**。
```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'` | 权限节点:短名=当前控制器动作,跨控制器写全节点(与注册节点逐字一致,详见 5.7) |
| `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', // 短名=当前控制器动作(等价 'system.timer_config/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 中转声明:
```js
[{
class: 'layui-btn layui-btn-normal layui-btn-sm',
method: 'tab',
text: '绑定',
title: '扫码绑定',
auth: 'app.code_bind/scan', // 全节点,与注册节点逐字一致
url: 'app.code_bind/scan', // 跨控制器 URL
icon: 'fa fa-link'
}]
```
`ua.auth` 按全局允许集(layout 注入的 `window.UA_AUTH`)判定:有权限显示按钮,无权限自动隐藏。
**全节点写法约定**:
- 与 `@NodeAnnotation` 注册的节点**逐字一致**:`ua.auth` 做字符串匹配,前端不做任何规范化转换
- 动作段**大小写敏感**:注册为 `setMaster` 就写 `'system.host/setMaster'`,禁止 snake 化写成 `set-master`(parseNodeStr 只规范控制器段,动作段原样保留)
- 短名(不含 `/`)自动按"当前控制器/短名"解析,只适用于本控制器动作
**未注册动作的行为差异**:权限载荷不含 default_pass,前端对"不在允许集"的按钮一律保守隐藏。在 `default_auth_check=true` 的部署中,未注册动作后端仍按真实规则放行,即前端隐藏而后端放行,此时应前后端核对并补注册节点。旧协议(HTML `data-auth` 属性中转)仅作为下游旧页面的回退通道保留,新页面不使用。
### 5.8 operat 数组结构
`operat` 是一个数组,元素有两种形态,可混用:
```js
operat: [
[/* 按钮组1:每个对象是一个按钮 */],
[/* 按钮组2 */],
'edit', // 字符串简写
'delete'
]
```
每个 `[]` 内的对象是独立按钮,按顺序渲染。`renderOperat`(定位见第十章速查表)做权限整列裁剪:只要任意一个按钮组内有按钮有权限,整列就保留。
## 六、页面分类标准(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` | 控制器方法注解,生成权限节点(前端 `ua.auth` 全局判定按钮显隐的依据) |
**关联搜索示例**(`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:权限节点不匹配(按钮不显示)**
- JS 里的 auth 值解析出的节点必须与控制器 `@NodeAnnotation` 注册的节点一致:短名等于"当前控制器/动作",跨控制器直接写全节点
- 全节点与注册节点逐字比对,动作段大小写敏感(注册 `setMaster` 写成 `set-master` 即失配隐藏)
- 未注册动作前端保守隐藏;`default_auth_check=true` 部署下后端仍按真实规则放行,前后端核对后补注册节点
排查工具:`php think admin:permission:nodes`(见 [ulthon-permission-cli](./ulthon-permission-cli/SKILL.md))
### 9.3 排错指南
**"按钮不显示"排查步骤**:
1. 浏览器控制台核对 `window.UA_AUTH`:确认按钮 auth 值解析出的节点在允许集内(短名自动补当前控制器前缀,全节点逐字匹配)
2. 检查 JS 里 auth 值与 `@NodeAnnotation` 注册节点是否逐字一致(动作段大小写敏感,禁止 snake 化动作段)
3. 用超管账号测试排除账号权限配置问题(超管 `all:true` 恒显)
4. 查看权限节点列表:`php think admin:permission:nodes`
5. 下游旧页面(data-auth 回退通道):检查 HTML 是否声明了对应的 `data-auth-X` 属性且值为 1。仅旧页面有此声明,新协议页面 index.html 无权限声明
**"搜索不生效"排查步骤**:
1. 浏览器抓 network 看请求是否带 `filter` 和 `op` 参数
2. 检查 `filter` JSON 是否包含期望的字段
3. 检查对应字段的 `op` 值是否符合预期(默认 `%*%`)
4. 后端用 `php think tools:db:query "SELECT ... WHERE ..."` 直接查表确认数据存在
5. 检查控制器是否调用了 `buildTableParames()` 而不是手动 `where()`
## 十、实测代码引用表
每个特性标注实测来源(文件 + grep 关键词定位;行号随迭代漂移,故不逐条标注行号),若代码与本文档冲突,以代码为准。
| 特性 | 实测来源 |
|------|---------|
| `ua.table.render` 入口 | `public/static/plugs/ulthon-admin/admin-table.js`(`grep "render: function (options)"`) |
| `formatCols`(cols 加工) | `admin-table.js`(`grep "formatCols: function"`) |
| `renderOperat`(操作列整列裁剪) | `admin-table.js`(`grep "renderOperat(data, elem)"`) |
| `renderToolbar`(顶部工具栏) | `admin-table.js`(`grep "renderToolbar: function"`) |
| `renderSearch`(搜索表单生成) | `admin-table.js`(`grep "renderSearch: function"`) |
| `search` 默认 `true` | `admin-table.js`(`grep "d.search, true"`) |
| `searchOp` 默认 `'%*%'` | `admin-table.js`(`grep "d.searchOp"`) |
| `search: 'number_limit'` / `'time_limit'` | `admin-table.js`(`grep "number_limit"`) |
| `listenTableSearch`(filter+op 构造) | `admin-table.js`(`grep "listenTableSearch: function"`) |
| `tool` 函数(自动填充默认 operat) | `admin-table.js`(`grep "operat = option.operat"`,tool 函数内) |
| `tool` 内置 'edit' / 'delete' 处理 | 同上 tool 函数内 `case 'edit'` / `case 'delete'` 分支 |
| `operat._if` 显隐判定 | `admin-table.js`(`grep "operat._if"`) |
| `operat.auth` 默认 `'add'` | `admin-table.js`(`grep "operat.auth = operat.auth \|\|"`) |
| `list` 渲染器 | `admin-table.js`(`grep "list: function"`) |
| `filePreview` 渲染器 | `admin-table.js`(`grep "filePreview: function"`) |
| `image` 渲染器(imageSplit/imageJoin) | `admin-table.js`(`grep "image: function"`) |
| `url` 渲染器(urlNameField) | `admin-table.js`(`grep "url: function"`) |
| `switch` 渲染器(自动调 modify) | `admin-table.js`(`grep "switch: function"`),modify 调用在 `grep "listenSwitch: function"` |
| `date` 渲染器 | `admin-table.js`(`grep "date: function"`) |
| `ua.getDataBrage` 实现 | `admin-utils.js`(`grep "ua.getDataBrage = function"`) |
| `#data-brage` 隐藏元素 | `extend/base/admin/view/layout/default.html`(layout 文件) |
| `data_brage` assign | `AdminControllerBase.php`(`grep "assign('data_brage'"`) |
| `buildTableParames`(filter+op 解析) | `AdminControllerBase.php`(`grep "function buildTableParames"`) |
| `$relationSearch` 属性 | `AdminControllerBase.php`(`grep "protected $relationSearch"`,默认 false) |
| `$selectWhere` 属性 | `AdminControllerBase.php`(`grep "protected $selectWhere"`) |
| `selectList()` 方法 | `AdminControllerBase.php`(`grep "public function selectList"`) |
| 实际示例:标准 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` |
| 受控默认数据流 / 逃生口分叉 | `grep "controlled !== false" public/static/plugs/ulthon-admin/admin-table.js`(注入门控、数据流分叉、store 接线共三处) |
| 受控接线(注册 store、自建分页与模板容器、首屏取数) | `grep "受控接线" admin-table.js` |
| 逃生口分支(引擎 parseData 装配保留) | `grep "逃生口" admin-table.js` |
| reloadData shim(受控分流/透传) | `grep "__uaOriginalReloadData" admin-table.js` |
| `ua.table.reloadData` / `ua.table.checkStatus` 分发入口 | `grep "受控/非受控分发入口" admin-table.js` |
| store 五入口(register/has/reload/fetch/checkStatus) | `grep "五个主入口" public/static/plugs/ulthon-admin/admin-table-store.js` |
| layoutList 规范化(字符串/对象条目) | `grep "normalizeLayoutList" admin-table.js` |
| 双端自动注入与视图按钮(未配 layoutList 注入 `['table','card']`) | `grep "autoInjected" admin-table.js` |
| 显式 defaultToolbar 时视图按钮补投(去重) | `grep "去重追加" admin-table.js` |
| 按端默认形态解析(记忆 > 按端显式 > 端缺省三层链) | `grep "defaultLayoutName" admin-table.js` |
| 形态记忆读/写(`table-layout-default`) | `grep "table-layout-default" admin-table.js` |
| 五级钩子优先级解析 | `grep "resolveLayoutEngine" admin-table.js` |
| 内置 card 模板(tplFunction) | `grep "builtinCardTemplate" admin-table.js` |
| 渲染上下文构建器 | `grep "buildLayoutContext" admin-table.js` |
| tplFunction/laytpl/Vue 调用约定注释 | `grep "列表级单实例" admin-table.js` |
| renderLayoutForm(模板管线入口) | `grep "renderLayoutForm: function" admin-table.js` |
| 统一行选中委托(data-table-id + data-id) | `grep "统一行选中委托" admin-table.js` |
| getLayoutForm / setLayoutForm | `grep "getLayoutForm" admin-table.js` |
| 内置卡片公共样式(`.ua-data-card` 族) | `grep "ua-data-card" public/static/admin/css/public.css` |
| `ua.checkMobile` 判定 | `grep "checkMobile" public/static/common/js/app.js`(`admin-utils.js` 转发) |
| 实际示例:三形态 + 按端默认 | `extend/base/admin/view/system/admin/index.js`(render 配置块:layoutList 三形态 + layoutDefault 对象式)+ `index.html`(usercardTpl 模板节点);第二官方范例 `extend/base/admin/view/mall/goods/`(goodscardTpl + `layoutDefault: 'goodscard'` 字符串写法) |
## 十一、多形态渲染机制(受控数据流与形态协议)
`ua.table.render` 页面默认运行在受控数据流上,并默认获得表格/卡片两种形态,两者都是零配置即得。本章只讲机制:数据流与逃生口(11.1)、形态清单协议与解析链(11.2)、双端默认行为(11.3)。要为自己的页面定制专属卡片模板(适用判断、上下文契约、实战配方、设计模式库、坑与验收),见 [ulthon-card-template](./ulthon-card-template/SKILL.md) 技能。
### 11.1 受控数据流(默认)与逃生口
**受控是默认数据流**:`ua.table.render` 不传 `controlled` 即受控,页面数据由 `admin-table-store.js`(全局 `window.UA_TABLE_STORE`)接管,引擎只做纯渲染,不再自己发请求。首屏、搜索、排序、翻页、switch 回滚、删除刷新等所有数据重载统一走 reloadData 的 shim 分流进 store。
| 组件 | 说明 |
|------|------|
| shim(`table.reloadData` 包装) | 受控表(store 已注册该 id)→ `store.reload`;非受控表原样透传引擎 |
| `ua.table.reloadData(id, opts)` | 页面代码刷新表格的推荐入口:按受控与否自动分流,受控/非受控页面通吃 |
| `ua.table.checkStatus(id)` | 取选中行的推荐入口,同样自动分流 |
旧页面里直呼 `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`。业务页面一般不直接调 store,用上表两个 `ua.table.*` 分发入口就够了。
**受控页的模板渲染容器**:受控表在引擎视图与分页条之间有一个模板形态容器,元素 id 为 `ua-layout-{tableRenderId}`(默认页面即 `#ua-layout-currentTableRenderId`,取的是 render 实例 id,不是表格元素 id,写选择器时勿混淆)。模板形态的内容渲染在这里,与引擎数据区互斥显隐;工具栏与搜索区两形态共用(卡片形态下新增/批量删除/搜索照常可用)。
**逃生口 `controlled: false`**:受控数据流覆盖绝大多数页面,少数下游页面依赖引擎 url 模式的行为,典型场景是自定义了 `parseData` 改写响应结构、或直接消费引擎请求管线。这类页面显式传 `controlled: false` 退回引擎 url 模式:引擎自己取数与分页,页面自定义 parseData 与选择态注入照常生效。注意逃回的代价:不注入形态清单、工具栏没有视图切换按钮、恒为表格形态,多形态渲染整体不可用;`ua.table.reloadData` 等分发入口对其透传引擎。
```js
// 逃生口示例:仅依赖引擎 url 模式行为的页面才加这一行
ua.table.render({
init: init,
controlled: false, // 退回引擎 url 模式,放弃多形态
cols: [[/* ... */]],
});
```
### 11.2 形态清单(layoutList)与双端自动注入
**双端自动注入**:受控页不配置 `layoutList` 时,框架双端自动注入 `['table', 'card']`,条目数大于 1,双端工具栏自动出现「视图」切换按钮。显式配置了 `layoutList` 的页面保持显式配置优先,不注入。`layoutList: ['table']` 单条目即锁定表格出口:无视图按钮,不可切形态。
`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 并回退表格形态,不白屏;回退不写记忆,修复后可再切换
**按端默认形态(layoutDefault)**:推荐对象式写法,按端声明默认形态。
```js
layoutDefault: {pc: 'table', mobile: 'card'}, // 官方示例页写法:按端取值
layoutDefault: 'card', // 旧写法兼容:字符串,双端同值
layoutDefault: true, // 旧协议"自动判断",视为未配置走端缺省
```
默认形态按三层链解析(优先级从高到低),端判定用 `ua.checkMobile()`:
1. **记忆**:localStorage 按页记忆,键 `table-layout-default`(经 `ua.getPageSetting`/`ua.setPageSetting` 带页面键前缀存储);值不在形态清单内忽略
2. **按端显式**:`layoutDefault` 按当前端取值,对象式 `pc`/`mobile` 键缺省视为该端未配置;取值不在清单内忽略
3. **端缺省**:PC 取 `table`,无则清单首条目;mobile 取 `card`,无则 `table`,再无则首条目
形态清单为空时恒为 `table`:非受控逃生口页,或 `layoutList` 配置了但所有条目都无效(垃圾清单),都不会在空清单上取首条目。
**视图切换与 CURD 缺省**:
- `layoutList` 条目数 > 1 时工具栏出现「视图」按钮;点击下拉条目即写记忆并切形态,切换用的是 store 当页数据,零网络请求
- 页面显式配置了 `defaultToolbar` 时,视图按钮会自动补投进最终工具栏(自动注入场景下去重追加),普通开发者无需关心
- 运行期 API:`ua.table.getLayoutForm(id)` 读当前形态、`ua.table.setLayoutForm(id, name)` 切形态。API 切换**不写记忆**,只有点视图下拉条目才写 `ua.setPageSetting`;API 切换后刷新会回到记忆形态,这是设计行为
- **CURD 页面缺省即得双形态**:CURD 生成的列表页不含任何受控/形态配置,自动获得受控数据流 + 双端「表格/卡片」+ 视图按钮,模板零改动;PC 端缺省表格、手机端缺省卡片
**两条硬约束**:
1. 模板渲染容器恒带 `layui-form` 类与 lay-filter(框架自建容器已保证,页面无需关心)。表单控件的渲染美化与事件都要求 `.layui-form` 祖先,模板里写 switch/checkbox 依赖这条保证,缺失则全部失效。
2. 模板作者侧另有一条位置硬约束(模板节点必须写在渲染容器之外,否则被模板引擎编译时吞掉),约束细节、页面写法与模板契约由 [ulthon-card-template](./ulthon-card-template/SKILL.md) 技能第三章承接。
### 11.3 双端默认行为
- 双端自动注入后 PC 端同样有视图切换按钮与卡片形态,端差异只体现在缺省形态上:PC 缺省 `table`,手机缺省 `card`(记忆与按端 layoutDefault 的优先级更高,见 11.2 解析链)
- 手机端判定 `ua.checkMobile()`(`admin-utils.js` 转发,实现位于 `public/static/common/js/app.js`):UA 关键字(Android/iPhone/iPad 等)或 `window.screen` 宽 < 600 且高 < 800。判的是物理屏幕指标,**浏览器开发者工具仅改视口宽度不会触发**;设备仿真验证手机默认形态时,必须同时覆写 UA 或仿真 screen 尺寸,仅设 viewport 无效
- 卡片形态能力边界:工具栏(刷新/新增/批量删除/导出)与搜索区两形态共用、照常可用;行选中、豁免与 `_disabled` 等模板侧语义由卡片模板契约定义(见 11.4);`_cells` 隐藏列(col.hide)不产出对应键,模板按原始字段直取即可
### 11.4 自定义模板指引
自定义卡片/模板形态的创作不在本技能展开,统一由 [ulthon-card-template](./ulthon-card-template/SKILL.md) 技能承接:渲染上下文契约(`_cells`/`_operatHtml`/`_checked`/`_disabled`、上下文冻结、列表上下文命名)、模板节点位置硬约束、行选中委托与豁免名单(勾位契约:点行内 `.ua-data-card-check` 勾位元素切换勾选,勾位元素须在行内,行根元素仍需携带 `data-table-id` + `data-id` 两属性)、取值通道三规则、usercardTpl/goodscardTpl 范例走读、实战配方、设计模式库、模板作者坑与验收 checklist。本技能只负责机制如何把模板跑起来;两个官方范例的 render 配置块(layoutList 三形态、layoutDefault 对象式与字符串式)见第十章引用表末行,可作机制示范直接对照。
## 相关技能
- [ulthon-card-template](./ulthon-card-template/SKILL.md):自定义卡片模板的创作(上下文契约、实战配方、设计模式库、坑与验收);本技能第十一章是其机制前提
- [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):权限节点核对(按钮 auth 值与 `@NodeAnnotation` 注册节点对不上时)
- [ulthon-tools-http-call](./ulthon-tools-http-call/SKILL.md):CLI 模拟请求验证列表接口
- [ulthon-page-qa](./ulthon-page-qa/SKILL.md):页面验证(AI 自测)与浏览器断言要点