Files
ulthon_admin/.agents/skills/ulthon-page-qa/SKILL.md

318 lines
20 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 按钮显隐:UA_AUTH 全局权限判定
按钮显隐由全局权限载荷决定,index.html 纯结构零权限声明:
1. **layout 注入全局载荷**:页面加载时服务端一次性注入 `window.UA_AUTH = {all, controller, nodes}`(`all`=超管标记;`controller`=当前控制器节点名;`nodes`=允许集 `{'控制器': ['动作', ...]}`,超管 `all:true` 时 nodes 为空数组)
- 实测:`extend/base/admin/view/layout/default.html`(内联 script,与 window.CONFIG 同级)
- 同体 JSON 模式(get_page_data)不携带该载荷,仅在页面 HTML 中存在
2. **ua.auth(node) 全局判定**:按钮对象带 `auth` 值,框架经 `ua.checkAuth` 依序判定:`'1'` 恒显 → `CONFIG.IS_SUPER_ADMIN` → `ua.auth(node)` → `data-auth-{node}` 属性回退(仅旧页面)
- 短名(不含 `/`)自动补 `UA_AUTH.controller` 前缀;全节点逐字匹配,动作段大小写敏感(如 `system.host/setMaster` 禁止写成 `set-master`)
- 默认值:`operat.auth = operat.auth || 'add'`(`admin-table.js:1105`)——未写 `auth` 的按钮按 `add` 节点判定
**QA 校验口径**:
- 页面源码断言 `window.UA_AUTH` 存在且形状正确(`{all, controller, nodes}` 三键齐全)
- 按钮显隐对应用户允许集:允许集内的按钮出现,不在允许集的消失,超管全显
- 未注册动作按钮前端保守隐藏(载荷无 default_pass);`default_auth_check=true` 部署下后端仍按真实规则放行,属已知差异,以前后端核对为准
- `data-auth-*` HTML 属性是下游旧页面的回退通道,新协议页面无此声明
> 验证口诀:按钮节点在 `UA_AUTH.nodes` 允许集内则显示,不在则隐藏(超管恒显)。节点路径必须和控制器 `@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. 工具栏按钮:显隐与用户允许集一致(`window.UA_AUTH`),有权限的出现、无权限的消失;旧页面回退通道才存在 `data-auth` 属性
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` |
| `window.UA_AUTH` 载荷注入(layout 内联) | `extend/base/admin/view/layout/default.html` |
| set-master 按钮全节点写法(勘正:旧 `data-auth-set-master` 属性是"自定义键/驼峰动作段"样例而非跨控制器样例,auth() 参数与本页同控制器;属性已随新协议清理,JS 现写全节点) | `extend/base/admin/view/system/host/index.js:33` |
| 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):按钮 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` 支持函数与字符串两种形式;未声明时默认恒真(总是显示)