mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-08-30 12:45:32 +08:00
docs(agents): 新增后台表格机制与页面验证技能
- ulthon-admin-table 技能:后台表格完整指南(核心机制/init/cols/内置渲染器/按钮系统/搜索系统/后端配套/常见模式与排错/实测代码引用表),含页面分类标准 A/B/C/D 与项目2 实战经验 - ulthon-page-qa 技能:AI 自测页面验证 6 步 checklist(HTML 冒烟→API→Page Data→浏览器→搜索→分辨率),零项目依赖原则,三模式验证策略
This commit is contained in:
660
.agents/skills/ulthon-admin-table/SKILL.md
Normal file
660
.agents/skills/ulthon-admin-table/SKILL.md
Normal file
@@ -0,0 +1,660 @@
|
||||
---
|
||||
name: "ulthon-admin-table"
|
||||
description: "后台列表页表格机制与定制完整指南:核心机制(ua 命名空间、三件套)、字段配置、内置渲染器、按钮系统(含页面分类标准 A/B/C/D)、搜索系统(前端配置 + 后端 buildTableParames)、后端配套、常见模式与排错。需要业务定制 CURD 生成的列表页(加按钮、改字段、调搜索)或排查按钮/搜索不生效时调用。"
|
||||
---
|
||||
|
||||
# 后台表格机制与定制(ulthon-admin-table)
|
||||
|
||||
本技能是后台列表页(基于 layui table 的 `ua.table.render`)完整指南。涵盖核心机制、字段配置、按钮系统、搜索机制、后端配套,以及实战中的常见模式与排错。
|
||||
|
||||
CURD 生成器生成列表页骨架后,所有业务定制(按钮、字段渲染、搜索)都在本技能范畴内。生成流程本身见 [ulthon-scheme-curd-workflow](./ulthon-scheme-curd-workflow/SKILL.md)。
|
||||
|
||||
## 何时调用
|
||||
|
||||
- 业务定制 CURD 生成的列表页(加/改字段、加按钮、调搜索)
|
||||
- 排查"按钮不显示"、"搜索不生效"、"字段渲染不对"
|
||||
- 自定义字段渲染(图片、状态、关联数据)
|
||||
- 实现状态流转按钮(带 `_if` 显隐)
|
||||
- 跨控制器跳转按钮
|
||||
|
||||
## 一、核心机制总览
|
||||
|
||||
### 1.1 `ua` 命名空间
|
||||
|
||||
`ua` = ulthon-admin 的 JS 全局对象,定义在 `public/static/plugs/ulthon-admin/`:
|
||||
|
||||
| 文件 | 作用 |
|
||||
|------|------|
|
||||
| `admin-table.js` (1650 行) | 表格核心:render / formatCols / renderOperat / 内置渲染器 / 搜索监听 |
|
||||
| `admin-core.js` | ua 全局组装、headers、url、parame 等工具 |
|
||||
| `admin-utils.js` | `ua.getDataBrage` 等数据读取工具 |
|
||||
| `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` |
|
||||
|
||||
## 相关技能
|
||||
|
||||
- [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 模拟请求验证列表接口
|
||||
319
.agents/skills/ulthon-page-qa/SKILL.md
Normal file
319
.agents/skills/ulthon-page-qa/SKILL.md
Normal file
@@ -0,0 +1,319 @@
|
||||
---
|
||||
name: "ulthon-page-qa"
|
||||
description: "页面验证(AI 自测)技能。改完/新建一个后台页面后,用规则 checklist 验证页面能开、数据对、按钮显隐对、搜索对、分辨率不崩。零项目依赖(不在项目里装 Playwright/npm/node)。需要验证 CURD 生成的页面或排查页面渲染问题时调用。"
|
||||
---
|
||||
|
||||
# 页面验证(AI 自测)— ulthon-page-qa
|
||||
|
||||
> 框架级技能(`ulthon-` 前缀)。一份规则驱动的 checklist,让 AI 智能体用**自己的**浏览器/HTTP 能力验证一个后台页面是否正常——**不在项目里安装任何新依赖**(不装 Playwright / npm / node)。
|
||||
|
||||
## 何时调用
|
||||
|
||||
- 改完/新建一个后台页面(控制器 + 视图 `*.html` + 同名 `*.js`)后,需要快速验证「页面能开、数据对、按钮显隐对、搜索对、分辨率不崩」
|
||||
- 排查「页面白屏 / 列不渲染 / 下拉空 / 按钮该出现没出现 / 搜索提交后查不到」等问题时
|
||||
- 作为 [ulthon-scheme-curd-workflow](./ulthon-scheme-curd-workflow/SKILL.md) 流程第 6~7 步「功能验证」的页面级执行细则
|
||||
|
||||
## 核心原则(不可协商)
|
||||
|
||||
1. **零项目依赖**:本技能是规则 checklist,不向项目 `package.json` / `composer.json` 引入任何东西。前端验证用**智能体自带的浏览器能力**(Playwright MCP / 内置 browser),不要在项目里写浏览器自动化代码
|
||||
2. **后端先于前端**:先用 `php think tools:http:call`(CLI,零浏览器)验后端三模式,后端对了再开浏览器验前端
|
||||
3. **特性名以代码为准**:下文每个特性都注明了实测来源(`文件:行`)。若代码与本文档冲突,以代码为准并反馈修订本文件
|
||||
|
||||
---
|
||||
|
||||
## 一、框架页面特性清单(均经实测,附代码出处)
|
||||
|
||||
本框架的「标准后台列表页」由 `ua.table.render({...})` 驱动(`public/static/plugs/ulthon-admin/admin-table.js`)。下列特性是验证时必须逐一对照的检查点。
|
||||
|
||||
完整机制(按钮/字段/搜索)详见 [ulthon-admin-table](./ulthon-admin-table/SKILL.md)。本技能聚焦"如何验证"。
|
||||
|
||||
### 1.1 cols 列定义
|
||||
|
||||
表格列在 `*.js` 的 `ua.table.render({ cols: [[ ... ]] })` 中声明。
|
||||
|
||||
实测示例(`extend/base/admin/view/system/admin/index.js:3-36`):
|
||||
|
||||
```js
|
||||
cols: [[
|
||||
{ type: "checkbox" },
|
||||
{ field: 'id', width: 80, title: 'ID' },
|
||||
{ field: 'username', minWidth: 80, title: '登录账户' },
|
||||
{ field: 'head_img', minWidth: 80, title: '头像', search: false, templet: ua.table.image },
|
||||
{ field: 'status', title: '状态', width: 85, search: 'select',
|
||||
selectList: { 0: '禁用', 1: '启用' }, templet: ua.table.switch },
|
||||
{ field: 'create_time', minWidth: 80, title: '创建时间', search: 'range' },
|
||||
{
|
||||
width: 250, title: '操作', fixed: 'right', templet: ua.table.tool,
|
||||
operat: ['edit', [{ /* 设置密码 */ }], 'delete']
|
||||
}
|
||||
]]
|
||||
```
|
||||
|
||||
### 1.2 下拉数据:`select_list_*` + `ua.getDataBrage` + `#data-brage`
|
||||
|
||||
这是框架「后端字典 → 前端下拉」的标准通道,验证时务必三处对齐:
|
||||
|
||||
1. **后端 assign**:控制器里 `$this->assign('select_list_{名}', $字典数组, true);`
|
||||
- 实测:`extend/base/admin/controller/system/HostBase.php`(assign `select_list_status` / `select_list_is_master`)
|
||||
- 约定:键名以 `select_list_` 开头,第三个参数 `true` 表示「同时压入 data_brage 通道」
|
||||
2. **布局序列化**:所有 assign 变量被打包成 JSON,渲染进隐藏元素
|
||||
- 实测:`extend/base/admin/view/layout/default.html` → `<script style="display: none;" id="data-brage" type="text/plain">{$data_brage|raw|default='[]'}</script>`
|
||||
- 来源:`AdminControllerBase::fetch()` 内 `$this->assign('data_brage', json_encode($this->dataBrage))`(`extend/base/common/controller/AdminControllerBase.php:273`)
|
||||
3. **前端读取**:`ua.getDataBrage('select_list_{名}')` 从 `#data-brage` 解析取值
|
||||
- 实测:`public/static/plugs/ulthon-admin/admin-utils.js:63-74`
|
||||
|
||||
> 验证口诀:**控制器 assign 的 key** 必须等于 **cols 里 `ua.getDataBrage('xxx')` 的参数**,否则下拉为空。
|
||||
|
||||
### 1.3 搜索表单:`search` / `searchOp` / `filter` / `op`
|
||||
|
||||
搜索表单由 cols 自动生成(无需手写 HTML),核心参数(实测 `admin-table.js:300-369`):
|
||||
|
||||
- `d.search`:默认 **`true`**(`admin-table.js:307`)——即列默认可搜索,要关掉才需显式 `search: false`
|
||||
- 搜索类型:
|
||||
- 默认(无 `search` 或 `search:true`):文本输入框
|
||||
- `search: 'select'`:下拉,选项取自同列 `selectList`
|
||||
- `search: 'number_limit'`:数值区间(生成 `[field]min` / `[field]max`,op 为 `min`/`max`)
|
||||
- `search: 'time_limit'`:时间区间
|
||||
- `search: 'range'`:时间范围(实测 `app/admin/view/system/timer_log/index.js` 的 `start_time` 字段)
|
||||
- `d.searchOp`:默认 **`'%*%'`**(LIKE `%val%`,`admin-table.js:312`)。可按列覆盖
|
||||
- `d.defaultSearchValue` / `d.searchValue`:默认选中值,会从 URL query 自动回填
|
||||
|
||||
**提交时的参数名**:搜索表单提交时把条件拼成两个对象 `formatFilter` / `formatOp`(`admin-table.js:300-301`),最终以 `filter[字段]=值` 和 `op[字段]=操作符` 形式发给后端(如 `filter[status]=2&op[status]=%*%`)。验证时检查 network 里这两个参数是否正确即可。
|
||||
|
||||
完整 searchOp 操作符表见 [ulthon-admin-table](./ulthon-admin-table/SKILL.md) 第七章。
|
||||
|
||||
### 1.4 按钮显隐:`data-auth-*`(按权限节点)
|
||||
|
||||
权限按钮由两处配合控制:
|
||||
|
||||
1. **HTML 写入权限标记**:表格容器上以 `data-auth-{动作}="{:auth('控制器/动作')}"` 形式声明每个动作的权限值(1 有权限 / 0 无)
|
||||
- 实测:`extend/base/admin/view/system/admin/index.html:3`:
|
||||
```html
|
||||
<table id="currentTable" ...
|
||||
data-auth-add="{:auth('system.admin/add')}"
|
||||
data-auth-edit="{:auth('system.admin/edit')}"
|
||||
data-auth-delete="{:auth('system.admin/delete')}"
|
||||
data-auth-password="{:auth('system.admin/password')}"
|
||||
data-auth-modify="{:auth('system.admin/modify')}"
|
||||
lay-filter="currentTable">
|
||||
```
|
||||
- `{:auth(...)}` 是模板函数,返回当前登录账号对该权限节点的布尔判定
|
||||
2. **JS 按 auth 匹配显隐**:cols/toolbar 里的按钮对象带 `auth: '{动作}'`,框架读取对应 `data-auth-{动作}` 值决定渲染与否
|
||||
- 实测:`system/admin/index.js`(toolbar 默认含 add;操作列含 `password` 自定义按钮 + edit + delete)
|
||||
- 默认值:`operat.auth = operat.auth || 'add'`(`admin-table.js:1105`)——未写 `auth` 的按钮按 `add` 节点判定
|
||||
|
||||
> 验证口诀:**`auth: 'X'`**(JS)必须能在表格容器找到对应的 **`data-auth-X`**(HTML),且该值=1,按钮才会出现。节点路径必须和控制器 `@NodeAnnotation` 注解一致。
|
||||
|
||||
### 1.5 按钮显隐:`_if`(按数据状态)
|
||||
|
||||
操作列按钮可按当前行数据动态显隐,属性名为 **`_if`**(下划线前缀,小写)。实测 `admin-table.js:1113-1124`:
|
||||
|
||||
```js
|
||||
operat._if = operat._if || function () { return true; };
|
||||
if (typeof operat._if == 'function') {
|
||||
if (operat._if(data, operat) !== true) { return ''; } // 不渲染该按钮
|
||||
} else if (typeof operat._if == 'string') {
|
||||
var ifValue = ua.table.returnColumnValue(data, operat._if, false);
|
||||
if (!ifValue) { return ''; }
|
||||
}
|
||||
```
|
||||
|
||||
- **函数形式**:实测 `app/admin/view/system/timer_config/index.js:56-58`——仅当 `run_type==='manual'` 时显示「触发」按钮(通过 CSS 控制显隐的另一种写法)
|
||||
- **字符串形式**:传字段名,该字段为真才显示
|
||||
- 未写 `_if` 时默认恒真(总是显示)
|
||||
|
||||
> 验证口诀:`_if` 返回的判定必须与业务约束一致;典型场景是「只有已绑定(status=X)才允许解绑」这类状态门禁。
|
||||
|
||||
### 1.6 页面接口同体——三模式触发条件(实测)
|
||||
|
||||
同一路由通过请求特征返回 HTML / API JSON / Page Data JSON 三种输出。**触发条件以 `AdminControllerBase::fetch()` + `checkParseApi()` + `RequestBase::isAjax()` 实测为准**:
|
||||
|
||||
前置开关:`config('app.auto_parse_api')` 必须为 `true`(默认开启)。整个机制在该开关闭合时不生效。
|
||||
|
||||
| 模式 | 触发条件(实测) | `isAjax()` 返回 | 控制器最终输出 |
|
||||
|------|----------------|----------------|--------------|
|
||||
| **HTML 模式** | 请求头**不含** `Accept: application/json`(非 isJson) | 走常规 ajax 探测(一般 false) | `fetch()` 渲染并返回 HTML 页面 |
|
||||
| **API 模式**(分页/提交) | `Accept: application/json`(isJson=true)且**不带** `get_page_data` 参数 | `true` | index 走 ajax 分支返回分页 JSON;POST success/error 返回 `json_message` |
|
||||
| **Page Data 模式**(取 assign 字典) | `Accept: application/json` **且** 带 `get_page_data` 参数(值任意,存在即可) | `false` | `fetch()`→`checkParseApi()` 返回 true → 返回 `json_message(assign 全部变量)` |
|
||||
| **错误提示** | 带 `get_page_data` 但**不带** `Accept: application/json` | — | `fetch()` 直接返回 `json_message([], 400, '使用 get_page_data 获取页面数据时,请设置请求头 Accept: application/json')`(`AdminControllerBase.php:269-271`) |
|
||||
|
||||
详细机制见 [ulthon-page-api-dual-mode](./ulthon-page-api-dual-mode/SKILL.md)。
|
||||
|
||||
---
|
||||
|
||||
## 二、AI 验证 checklist(改完页面后的标准 6 步)
|
||||
|
||||
每次完成一个后台页面开发/修改,按下列顺序执行。前 3 步纯 CLI(零浏览器、零项目依赖),后 3 步用智能体自带浏览器能力。
|
||||
|
||||
### 步骤 1 — 页面可打开(HTML 冒烟)
|
||||
|
||||
- 用 `tools:http:call` 以 **HTML 模式**请求页面 URL(不带 `Accept:json`),断言 HTTP 200、响应是 HTML、无 PHP Notice / 模板变量未定义错误
|
||||
- 重点看:`Undefined variable`、`TemplateNotFoundException`、SQL 报错
|
||||
|
||||
```bash
|
||||
php think tools:http:call --url="/admin/system.admin/index" --method=GET
|
||||
```
|
||||
|
||||
### 步骤 2 — 后端接口验证(API 模式)
|
||||
|
||||
- 以 **API 模式**请求 `index`(带 `Accept:json`、不带 `get_page_data`),断言返回 `json_message` 结构、分页 `data.list|data.data` 非空、字段与 cols 对应
|
||||
|
||||
```bash
|
||||
php think tools:http:call --url="/admin/system.admin/index" --method=GET --headers="{\"Accept\":\"application/json\"}"
|
||||
```
|
||||
|
||||
- 对增/改/状态流转接口:用 `--method=POST --data='{...}'` 验提交返回 success,再用 API 模式回查确认数据变化
|
||||
|
||||
### 步骤 3 — 页面数据验证(Page Data 模式)
|
||||
|
||||
- 以 **Page Data 模式**请求页面(`Accept:json` + `get_page_data=1`),断言返回的 assign 字典里 `select_list_*` 齐全、`init.*Url` 等地址存在
|
||||
|
||||
```bash
|
||||
php think tools:http:call --url="/admin/system.admin/index?get_page_data=1" --method=GET --headers="{\"Accept\":\"application/json\"}"
|
||||
```
|
||||
|
||||
- 或用专用参数:`tools:http:call --app=admin --controller=system.admin --action=index --page-data`
|
||||
|
||||
### 步骤 4 — 前端渲染验证(浏览器)
|
||||
|
||||
- 用智能体自带浏览器能力(Playwright MCP / agent-browser)打开页面(需登录态),逐项核对:
|
||||
1. 表格表头/列按 cols 渲染(字段、标题、关联字段显示正确)
|
||||
2. 搜索区下拉选项 = Page Data 模式里 `select_list_*` 的内容(对照步骤 3 结果)
|
||||
3. 工具栏按钮:`data-auth-X=1` 的按钮出现、`=0` 的消失;`auth: 'X'`(JS)与 `data-auth-X`(HTML)一一对应
|
||||
4. 操作列按钮:`_if` 条件成立才出现
|
||||
|
||||
### 步骤 5 — 搜索提交验证(网络抓包)
|
||||
|
||||
- 在浏览器里实际提交一次搜索,抓 network 请求,核对:
|
||||
- 请求头含 `Accept: application/json`、走 API 模式
|
||||
- 参数形如 `filter[字段]=值` + `op[字段]=操作符`(默认 `%*%` 即 LIKE)
|
||||
- `search:'select'` 的列确实以下拉提交、`number_limit`/`time_limit` 提交 `[field]min/[field]max`
|
||||
- 返回结果集与筛选条件一致
|
||||
|
||||
### 步骤 6 — 分辨率覆盖(布局回归)
|
||||
|
||||
按 `AGENTS.md` 规定的档位截图检查布局不崩:
|
||||
|
||||
- **桌面端 4 档**:1920x1080、1536x864、1366x768、1280x720
|
||||
- **移动端 4 档**:390x844(iPhone 12/13/14)、393x852(Pixel 7)、375x667(小屏 iPhone)、360x800(常见 Android)
|
||||
- **平板 / 折叠屏**:768x1024(iPad)
|
||||
- 重点看:表格横向滚动、操作列 `fixed:'right'` 是否遮挡、搜索区是否溢出
|
||||
|
||||
---
|
||||
|
||||
## 三、AI 工具使用指南
|
||||
|
||||
### 工具链顺序:CLI 后端 → 浏览器前端 → 视觉对比
|
||||
|
||||
```
|
||||
tools:http:call(三模式,零浏览器)
|
||||
│
|
||||
▼ 后端三模式全绿
|
||||
智能体自带浏览器(Playwright MCP / 内置 browser)
|
||||
│
|
||||
▼ 前端 DOM + 交互 + 截图全绿
|
||||
visual-qa 技能(视觉对比 / 回归断言)
|
||||
```
|
||||
|
||||
### 3.1 `php think tools:http:call`(后端,主力)
|
||||
|
||||
详见 [ulthon-tools-http-call](./ulthon-tools-http-call/SKILL.md)。要点:
|
||||
|
||||
- **默认 super token**:命令自动生成 token 写入登录缓存并带 `Authorization: Bearer <token>`,等价已登录。指定账号用 `--user-id=N`;回到未登录用 `--super-token=false`
|
||||
- **三模式映射**:
|
||||
- HTML 冒烟:不加 `Accept:json` 头
|
||||
- API 模式:`--headers='{"Accept":"application/json"}'`(不加 `get_page_data`)
|
||||
- Page Data 模式:`--page-data`(等价追加 `get_page_data=1` + Accept:json)
|
||||
- **登录态失效排查**:若仍提示「请先登录」,多为命令行与服务端缓存不共享;确认走同一套 `Cache::store('login')`
|
||||
- **无权限排查**:已登录但返回「无权限」,用 `--user-id` 换超管或检查 `admin:permission:nodes`
|
||||
|
||||
### 3.2 浏览器验证(前端,用智能体自带能力)
|
||||
|
||||
- **使用 Playwright MCP 或智能体内置 browser 工具**。加载 `playwright` 技能(`/playwright`)获取用法
|
||||
- **严禁**在项目里 `npm install playwright`、新建 e2e 测试目录、写 `.spec.js`——这是规则 checklist,不是项目测试工程
|
||||
- 需要登录态时:用 super token 思路,或直接在浏览器里走后台登录页登录一次后保持会话
|
||||
- 做的只有三件事:打开页面(DOM 检查)、触发交互(搜索/点按钮)、按分辨率截图
|
||||
|
||||
### 3.3 `visual-qa` 技能(视觉对比)
|
||||
|
||||
- 对步骤 4/6 的截图,调用 `/visual-qa` 做视觉回归断言(设计系统一致性 + 功能完整性 + CJK 文字裁切)
|
||||
- 适合在重构/换肤后确认「没改坏」
|
||||
|
||||
### 3.4 辅助:`tools:log` / `tools:db`
|
||||
|
||||
- 前端报错但后端返回正常时:`php think tools:log:show` 看运行时异常
|
||||
- 数据对不上时:`php think tools:db:query "SELECT ..."` 直接查表(仅调试,不改表结构)
|
||||
|
||||
---
|
||||
|
||||
## 四、页面接口同体——三模式验证策略(各模式验什么)
|
||||
|
||||
明确每个模式该验的内容,避免重复或遗漏:
|
||||
|
||||
### 4.1 API 模式(`Accept:json`,无 `get_page_data`)——验业务正确性
|
||||
|
||||
- **验什么**:查询逻辑(筛选/排序/分页/关联)、写入逻辑(增删改)、状态流转
|
||||
- **怎么验**:`tools:http:call` 带 `Accept:json`,断言 `code/msg/data` 结构、分页总数与 SQL 实际一致、状态流转前后数据变化符合预期
|
||||
- **不验**:模板渲染、下拉选项(那是 Page Data 模式的事)
|
||||
|
||||
### 4.2 Page Data 模式(`Accept:json` + `get_page_data=1`)——验 assign 字典完整性
|
||||
|
||||
- **验什么**:页面渲染所需的全部静态数据——`select_list_*` 下拉选项、`init.*Url` 地址、默认值、其它 assign 变量
|
||||
- **怎么验**:`tools:http:call --page-data`,断言每个 cols 里 `ua.getDataBrage('xxx')` 引用的 key 都在返回字典里且非空
|
||||
- **不验**:业务查询结果集(那是 API 模式的事)
|
||||
|
||||
### 4.3 HTML 模式(无 `Accept:json`)——验模板渲染正确性
|
||||
|
||||
- **验什么**:模板能编译、`{$变量}` 都有值、`{:auth(...)}` 等模板函数正常、无 PHP Notice、DOM 结构(表格容器、`#data-brage` 隐藏元素)存在
|
||||
- **怎么验**:`tools:http:call` 不带头,断言响应是 HTML、包含 `<table id="currentTable"` 与 `<script id="data-brage"`、无错误字符串
|
||||
- **不验**:JS 执行后的最终 DOM(那是步骤 4 浏览器的事)
|
||||
|
||||
---
|
||||
|
||||
## 五、附录
|
||||
|
||||
### 5.1 实测代码引用表
|
||||
|
||||
| 特性 | 实测来源 |
|
||||
|------|---------|
|
||||
| `ua.table.render` 入口 | `public/static/plugs/ulthon-admin/admin-table.js:14` |
|
||||
| cols 定义(checkbox/field/title/关联字段) | `extend/base/admin/view/system/admin/index.js:3-36` |
|
||||
| `search:'select'` + `selectList` | `extend/base/admin/view/system/admin/index.js:15`(status 字段) |
|
||||
| `search: 'range'` 时间范围 | `app/admin/view/system/timer_log/index.js:65`(start_time) |
|
||||
| search 默认 true / `searchOp` 默认 `%*%` | `admin-table.js:307`, `admin-table.js:312` |
|
||||
| `formatFilter`/`formatOp`(filter/op 提交) | `admin-table.js:300-301` |
|
||||
| search 类型 select/number_limit/time_limit | `admin-table.js:348-369` |
|
||||
| `ua.getDataBrage` 实现 | `public/static/plugs/ulthon-admin/admin-utils.js:63-74` |
|
||||
| `#data-brage` 隐藏元素 | `extend/base/admin/view/layout/default.html` |
|
||||
| `data_brage` assign | `extend/base/common/controller/AdminControllerBase.php:273` |
|
||||
| 后端 assign `select_list_*` | `extend/base/admin/controller/system/HostBase.php` |
|
||||
| `data-auth-{动作}` HTML 标记 | `extend/base/admin/view/system/admin/index.html:3` |
|
||||
| `data-auth-set-master` 跨控制器权限示例 | `extend/base/admin/view/system/host/index.html:5` |
|
||||
| toolbar/auth(默认 add) | `extend/base/admin/view/system/admin/index.js` |
|
||||
| 操作列 auth(password 自定义按钮) | `extend/base/admin/view/system/admin/index.js:24-31` |
|
||||
| `operat.auth` 默认 'add' | `admin-table.js:1105` |
|
||||
| `_if`(函数/字符串判定) | `app/admin/view/system/timer_config/index.js:56-58`;`admin-table.js:1113-1124` |
|
||||
| 自定义 templet(状态徽章) | `app/admin/view/system/timer_log/index.js:3-11`(statusTemplet) |
|
||||
| `fetch()` 三模式分流 | `extend/base/common/controller/AdminControllerBase.php:267-291` |
|
||||
| `checkParseApi()` Page Data 判定 | `AdminControllerBase.php:293-302` |
|
||||
| `get_page_data` 误用错误提示 | `AdminControllerBase.php:269-271` |
|
||||
| 开关 `app.auto_parse_api` | `AdminControllerBase.php:295` |
|
||||
|
||||
### 5.2 相关技能
|
||||
|
||||
- [ulthon-admin-table](./ulthon-admin-table/SKILL.md):表格机制完整说明(本技能验证的对象)
|
||||
- [ulthon-page-api-dual-mode](./ulthon-page-api-dual-mode/SKILL.md):页面/接口同体的触发条件与机制(本技能 1.6 节的依据)
|
||||
- [ulthon-tools-http-call](./ulthon-tools-http-call/SKILL.md):CLI HTTP 调用工具用法(本技能三模式的执行工具)
|
||||
- [ulthon-scheme-curd-workflow](./ulthon-scheme-curd-workflow/SKILL.md):标准开发流程,本技能是其「功能验证」环节的页面级细则
|
||||
- [ulthon-permission-cli](./ulthon-permission-cli/SKILL.md):`data-auth-*` / `auth` 注解节点对不上时,用它核对节点
|
||||
- `playwright` / `visual-qa`(内置技能):前端浏览器验证与视觉对比
|
||||
|
||||
### 5.3 计划假设修正记录
|
||||
|
||||
> 编写本技能前已逐条核对真实代码,结论:**计划假设全部准确,无需修正**。补充的实测细节如下:
|
||||
|
||||
- `get_page_data` 的**值**无关紧要(框架用 `has('get_page_data')` 判存在性),惯例仍写 `=1`
|
||||
- 搜索列 `search` **默认为 true**(非 false)——即列默认参与搜索,要关闭需显式 `search:false`
|
||||
- 默认搜索操作符 `searchOp` = `'%*%'`(两端 LIKE),提交时表现为 `op[字段]=%*%`
|
||||
- `ua.getDataBrage` 拼写为 **`Brage`**(非 Bridge),对应隐藏元素 `#data-brage` 与 assign 键 `data_brage`,三者命名一致
|
||||
- `_if` 支持函数与字符串两种形式;未声明时默认恒真(总是显示)
|
||||
Reference in New Issue
Block a user