Files
ulthon_admin/.agents/skills/ulthon-page-qa/SKILL.md
augushong 98dd244e61 docs(agents): 新增后台表格机制与页面验证技能
- ulthon-admin-table 技能:后台表格完整指南(核心机制/init/cols/内置渲染器/按钮系统/搜索系统/后端配套/常见模式与排错/实测代码引用表),含页面分类标准 A/B/C/D 与项目2 实战经验
- ulthon-page-qa 技能:AI 自测页面验证 6 步 checklist(HTML 冒烟→API→Page Data→浏览器→搜索→分辨率),零项目依赖原则,三模式验证策略
2026-07-19 12:00:21 +08:00

320 lines
19 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-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 分支返回分页 JSONPOST 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 档**390x844iPhone 12/13/14、393x852Pixel 7、375x667小屏 iPhone、360x800常见 Android
- **平板 / 折叠屏**768x1024iPad
- 重点看:表格横向滚动、操作列 `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` |
| 操作列 authpassword 自定义按钮) | `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` 支持函数与字符串两种形式未声明时默认恒真总是显示