docs(skill): 拆分表格与卡片双技能并同步最新代码

This commit is contained in:
augushong
2026-10-02 01:41:52 +00:00
parent 981a6d8baa
commit 5b28bc3fa5
2 changed files with 125 additions and 172 deletions

View File

@@ -1,6 +1,6 @@
---
name: "ulthon-admin-table"
description: "后台列表页表格机制与定制完整指南:核心机制(ua 命名空间、三件套)、字段配置、内置渲染器、按钮系统(含页面分类标准 A/B/C/D)、搜索系统(前端配置 + 后端 buildTableParames)、后端配套、常见模式与排错、多形态渲染(受控默认数据流与 controlled:false 逃生口、layoutList 五级模板协议 tplFunction/tplLay/tplVue/tplLayId/tplVueId、双端自动注入、按端 layoutDefault、卡片形态、渲染上下文)。需要业务定制 CURD 生成的列表页(加按钮、改字段、调搜索)、排查按钮/搜索不生效、自定义卡片/模板形态、或判断页面是否需要 controlled:false 逃生口时调用。"
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)
@@ -16,7 +16,7 @@ CURD 生成器生成列表页骨架后,所有业务定制(按钮、字段渲
- 自定义字段渲染(图片、状态、关联数据)
- 实现状态流转按钮(带 `_if` 显隐)
- 跨控制器跳转按钮
- 列表自定义卡片/模板形态与表格/卡片切换(`layoutList` + `layoutDefault`,见第十一章)
- 列表多形态机制与表格/卡片切换(`layoutList` + `layoutDefault`,见第十一章;自定义卡片模板的创作见 [ulthon-card-template](./ulthon-card-template/SKILL.md) 技能)
- 判断页面走的是哪种数据流,或是否需要 `controlled: false` 逃生口(见第十一章)
## 一、核心机制总览
@@ -219,7 +219,7 @@ var init = {
### 5.1 顶部工具栏(toolbar)
通过 `ua.table.render({ toolbar: [...] })` 配置。**不声明时默认 `['refresh', 'add', 'delete', 'export']`**——这意味着只读页面如果不覆盖,会显示无用的"新增/删除"按钮(见 9.2 常见错误)。
通过 `ua.table.render({ toolbar: [...] })` 配置。**不声明时默认 `['refresh', 'add', 'delete', 'export']`**,这意味着只读页面如果不覆盖,会显示无用的"新增/删除"按钮(见 9.2 常见错误)。
**字符串简写**(自动检查权限):
@@ -256,7 +256,7 @@ ua.table.render({
### 5.2 行级操作按钮(operat)
在 `cols` 最后一列配置 `templet: ua.table.tool` + `operat` 数组。**不声明 operat 时,tool 函数内自动填充 `['edit', 'delete']`**(admin-table.js:1055)。
在 `cols` 最后一列配置 `templet: ua.table.tool` + `operat` 数组。**不声明 operat 时,tool 函数内自动填充 `['edit', 'delete']`**。
```js
{
@@ -393,7 +393,7 @@ operat: [
]
```
每个 `[]` 内的对象是独立按钮,按顺序渲染。`renderOperat`(admin-table.js:769)做权限整列裁剪:只要任意一个按钮组内有按钮有权限,整列就保留。
每个 `[]` 内的对象是独立按钮,按顺序渲染。`renderOperat`(定位见第十章速查表)做权限整列裁剪:只要任意一个按钮组内有按钮有权限,整列就保留。
## 六、页面分类标准(A/B/C/D)
@@ -619,37 +619,36 @@ CURD 生成时会将关联模型的全部字段(如 `systemAdmin.*`)输出
## 十、实测代码引用表
每个特性标注实测来源(多为文件:行;受控/多形态相关特性因行号随迭代漂移,改用 grep 关键词定位),若代码与本文档冲突,以代码为准。
每个特性标注实测来源(文件 + grep 关键词定位;行号随迭代漂移,故不逐条标注行号),若代码与本文档冲突,以代码为准。
| 特性 | 实测来源 |
|------|---------|
| `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` |
| `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 | `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` |
| `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` |
@@ -671,12 +670,13 @@ CURD 生成时会将关联模型的全部字段(如 `systemAdmin.*`)输出
| 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/system/admin/index.js`(render 配置块:layoutList 三形态 + layoutDefault 对象式)+ `index.html`(usercardTpl 模板节点);第二官方范例 `extend/base/admin/view/mall/goods/`(goodscardTpl + `layoutDefault: 'goodscard'` 字符串写法) |
## 十一、多形态渲染(受控数据流与自定义模板)
## 十一、多形态渲染机制(受控数据流与形态协议)
`ua.table.render` 页面默认运行在受控数据流上,并默认获得表格/卡片两种形态,两者都是零配置即得。开发者只需要回答两个问题:想定制形态怎么配(`layoutList` + `layoutDefault`,见 11.2),以及自己的页面是否属于少数需要逃生口的场景(`controlled: false`,见 11.1)。官方完整范例是用户管理页:`extend/base/admin/view/system/admin/index.js` 顶部 render 配置里的 layoutList 三形态与 layoutDefault 对象式按端默认,建议直接对照源码。
`ua.table.render` 页面默认运行在受控数据流上,并默认获得表格/卡片两种形态,两者都是零配置即得。本章只讲机制:数据流与逃生口(11.1)、形态清单协议与解析链(11.2)、双端默认行为(11.3)。要为自己的页面定制专属卡片模板(适用判断、上下文契约、实战配方、设计模式库、坑与验收),见 [ulthon-card-template](./ulthon-card-template/SKILL.md) 技能。
### 11.1 受控数据流(默认)与逃生口
@@ -763,92 +763,30 @@ layoutDefault: true, // 旧协议"自动判断",视
- `layoutList` 条目数 > 1 时工具栏出现「视图」按钮;点击下拉条目即写记忆并切形态,切换用的是 store 当页数据,零网络请求
- 页面显式配置了 `defaultToolbar` 时,视图按钮会自动补投进最终工具栏(自动注入场景下去重追加),普通开发者无需关心
- 运行期 API:`ua.table.getLayoutForm(id)` 读当前形态、`ua.table.setLayoutForm(id, name)` 切形态
- 运行期 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. laytpl/Vue 的模板节点(`<script type="text/html">` / `text/x-template`)**必须写在渲染容器之外**(页面根容器之后即可)。节点若写进容器内会被模板引擎编译时吞掉。这是使用者必须遵守的约束:框架不生成也不搬运模板节点。
2. 模板作者侧另有一条位置硬约束(模板节点必须写在渲染容器之外,否则被模板引擎编译时吞掉),约束细节、页面写法与模板契约由 [ulthon-card-template](./ulthon-card-template/SKILL.md) 技能第三章承接。
### 11.3 渲染上下文
自定义模板拿到的数据由 `ua.table.buildLayoutContext(tableId)` 构建(调试时可直接在控制台调用查看)。
**行上下文**(tplFunction 第一参 / laytpl 的 `d` / Vue 的 `v-for` 项):
| 字段 | 说明 |
|------|------|
| `...row` | 行原始字段全部可用(username、status、create_time 等) |
| `_cells` | 对象,field → 该列 templet 渲染后的 HTML。只有配了渲染器的列才有内容;纯文本列恒为空串,勿引用,直接用原始字段 |
| `_operatHtml` | 操作列渲染产物(编辑/删除等按钮 HTML,已按 data-auth-* 裁剪权限) |
| `_checked` | 该行是否选中(store 选中集合) |
| `_disabled` | 该行是否禁选(行数据 LAY_DISABLED) |
**列表上下文**(tplFunction 第二参):`{tableId, rows, page, count, checkedAll}`。laytpl 形态挂在 `d._list` 上;Vue 形态叫 `list`,因为 Vue 实例不代理 `_`/`$` 前缀的数据键,裸写 `_list` 会 ReferenceError。
### 11.4 自定义形态示例
**laytpl 形态(tplLayId)**,简版:
```html
<!-- 页面模板区:渲染容器之外 -->
<script type="text/html" id="myCardTpl">
<div class="my-card">
<h3>{{- d._cells.title }}</h3>
<div>{{- d._operatHtml }}</div>
{{# if (d._checked) { }}<i class="layui-icon layui-icon-ok-circle"></i>{{# } }}
</div>
</script>
```
```js
layoutList: ['table', {name: 'mycard', title: '自定义卡', tplLayId: '#myCardTpl'}],
```
laytpl 语法实测(layui 2.10.1 内置 laytpl v2.0.0):`{{ d.x }}` 与 `{{= d.x }}` 都会 HTML 转义,`{{- d.x }}` 才是原样输出。`_cells`/`_operatHtml` 是渲染产物 HTML,必须用 `{{- }}`;纯文本用 `{{ }}` 转义更安全;`{{# }}` 写 JS 语句;列表上下文是 `d._list`。
**Vue 形态(tplVueId)**,简版:
```html
<!-- 页面模板区:渲染容器之外 -->
<script type="text/x-template" id="myVueTpl">
<div class="my-card" v-for="d in rows" :key="d.id"
:data-id="d.id" :data-table-id="list.tableId"
:class="{'ua-data-card-checked': d._checked}">
<h3 v-html="d._cells.title"></h3>
<div v-html="d._operatHtml"></div>
</div>
</script>
```
```js
layoutList: ['table', {name: 'mycard', title: '自定义卡', tplVueId: '#myVueTpl'}],
```
- `v-for="d in rows"` 承接行循环;`_cells`/`_operatHtml` 必须 `v-html` 输出,纯文本字段用 `{{ }}` 原生插值(自动转义);列表上下文是 `list`
- 行根元素带 `data-table-id` + `data-id` 两属性即获得统一行选中委托:点卡片切换勾选、选中态经 `:class` 绑 `ua-data-card-checked` 高亮回显;按钮/表单控件自动豁免不误触,零 JS
- 事件无需自己绑定:操作按钮(data-open/data-request 等属性)与 switch/checkbox 经页面级委托与 form 事件天然生效;`@click` 可用,但处理函数内不要修改模板数据(渲染上下文被冻结,数据更新一律整卡销毁重建)
**完整官方范例**:用户管理三形态(表格 + 名片 + 内置卡片),建议直接对照源码:
| 位置 | 内容 |
|------|------|
| `extend/base/admin/view/system/admin/index.js` | render 配置块:layoutList 三形态 + layoutDefault 对象式按端默认(`{pc: 'table', mobile: 'card'}`) |
| `extend/base/admin/view/system/admin/index.html` | usercardTpl 模板节点(`<script type="text/x-template">`,位于渲染容器之外)+ 页面级网格样式 |
该范例演示了三种典型取值通道:渲染器列(status)用 `_cells` + v-html 承接框架语义、纯文本列(username/create_time/phone)直接原始字段插值、头像(head_img)因名片需要圆形大图绕开渲染器直接绑原始字段。模板形态的网格布局由模板自带容器承担(Vue 形态不带内置卡片网格类)。
### 11.5 双端默认行为
### 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 无效
- 卡片形态能力边界:工具栏(刷新/新增/批量删除/导出)与搜索区两形态共用、照常可用;勾选语义由卡片勾位图标与选中委托承接(checkbox 列本身不进模板上下文);`_disabled` 行整元素不可勾选
- 卡片形态能力边界:工具栏(刷新/新增/批量删除/导出)与搜索区两形态共用、照常可用;行选中、豁免与 `_disabled` 等模板侧语义由卡片模板契约定义(见 11.4)
### 11.4 自定义模板指引
自定义卡片/模板形态的创作不在本技能展开,统一由 [ulthon-card-template](./ulthon-card-template/SKILL.md) 技能承接:渲染上下文契约(`_cells`/`_operatHtml`/`_checked`/`_disabled`、上下文冻结、列表上下文命名)、模板节点位置硬约束、行选中委托与豁免名单、取值通道三规则、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):权限节点核对(`data-auth-X` 与 `@NodeAnnotation` 对不上时)
- [ulthon-tools-http-call](./ulthon-tools-http-call/SKILL.md):CLI 模拟请求验证列表接口
- [ulthon-page-qa](./ulthon-page-qa/SKILL.md):页面验证(AI 自测)与浏览器断言要点

View File

@@ -1,13 +1,13 @@
---
name: "ulthon-card-template"
description: "自定义卡片/模板形态动手配方:面向用自己的列表页定制专属卡片形态的下游开发者。涵盖适用判断、layoutList 五级模板协议选型(tplFunction/tplLay/tplVue/tplLayId/tplVueId)、渲染上下文契约(_cells/_operatHtml/_checked/_disabled 与上下文冻结)、以商品卡为蓝本的 step-by-step 配方、设计模式库(头部三件套/图面悬浮开关/价格层级/状态徽标/底部分隔/图片降级/响应式/统一设计语言)、取值通道三规则、坑与红线、验收 checklist。需要定制卡片/自定义卡片模板/卡片模板/商品卡/名片/多形态模板/layoutList 自定义条目/tplVueId,或排查自定义模板不渲染、点卡片误勾选、模板数据被改无效时调用。机制原理(受控数据流、五级协议引擎、双端注入、layoutDefault 三层链)见 ulthon-admin-table 技能第十一章,本技能定位是动手配方,不复述机制。"
description: "自定义卡片/模板形态创作指南:面向想让列表页变好看、为自己的列表页定制专属卡片形态的下游开发者。涵盖适用判断(内置 card 兜底 vs 定制时机、锁定纯表格)、五级模板协议选型建议(官方范例为何都选 tplVueId)、渲染上下文契约(_cells/_operatHtml/_checked/_disabled 与上下文冻结、模板节点容器外硬约束、行选中委托与豁免名单)、以商品卡为蓝本的 step-by-step 配方、设计模式库(头部三件套/图面悬浮开关/价格层级/状态徽标/底部分隔/图片降级/响应式/统一设计语言)、取值通道三规则、坑与红线、验收 checklist。需要定制卡片/自定义卡片模板/卡片模板/商品卡/名片/自定义形态/layoutList 自定义条目/tplVueId,或想让列表变好看、排查自定义模板不渲染/点卡片误勾选/模板内开关失效时调用。多形态机制(受控数据流、五级协议引擎、双端注入、layoutDefault 三层链)见 ulthon-admin-table 技能第十一章,本技能定位是动手创作,不复述机制。"
---
# 自定义卡片模板定制指南(ulthon-card-template)
# 自定义卡片模板创作指南(ulthon-card-template)
本技能教下游开发者为自己的列表页定制卡片/模板形态。全部配方来自两个实战模板:用户管理名片(`extend/base/admin/view/system/admin/index.html` 的 usercardTpl)与商品列表商品卡(`extend/base/admin/view/mall/goods/index.html` 的 goodscardTpl),两者都是框架分发、可直接对照的官方范例。
机制原理(受控数据流为什么零请求、五级协议引擎如何回退、layoutDefault 三层解析链)不在本技能展开,统一见 [ulthon-admin-table](../ulthon-admin-table/SKILL.md) 技能第十一章。本技能回答的是"怎么动手写"。
**机制前提**:多形态机制(受控数据流为什么零请求、layoutList 五级协议与回退、双端自动注入、layoutDefault 三层解析链、模板渲染容器的构建与命名)统一由 [ulthon-admin-table](../ulthon-admin-table/SKILL.md) 技能第十一章定义,本技能不复述。本技能回答的是"怎么动手写"。
## 一、适用判断:先想清楚要不要定制
@@ -19,45 +19,33 @@ description: "自定义卡片/模板形态动手配方:面向用自己的列
- 需要品牌感:自定义配色、圆角、卡片结构(头部三件套、悬浮开关等)
- 需要字段编排控制:哪些字段大字强调、哪些弱化、哪些组合进徽标,与表格列语义不同
**锁定纯表格的出口**:如果页面明确不需要任何卡片形态(例如纯运维日志类页面),配单条目清单即可,工具栏不会出现视图按钮:
**锁定纯表格的出口**:页面明确不需要任何卡片形态时(例如纯运维日志类页面),配单条目清单即可:
```js
// 场景出处:本轮 QA 实证(table-card-global-enabled 计划 Task9 场景 2,host 页临时配置验证)
layoutList: ['table'],
```
**一个例外提醒**:显式传 `controlled: false` 逃生口的页面(依赖引擎 url 模式行为的深度定制页,如自定义 parseData)整条多形态链路都不可用,配了 `layoutList` 也不会有视图按钮。判断自己是否属于这类页面见 ulthon-admin-table 第十一章 11.1。
单条目锁定的语义与判断逻辑见 ulthon-admin-table 第十一章 11.2。
## 二、五级协议选型速查
**一个例外提醒**:若页面显式传 `controlled: false` 走逃生口(依赖引擎 url 模式行为的深度定制页,如自定义 parseData),不要配 `layoutList`。是否属于这类页面的判断见 ulthon-admin-table 第十一章 11.1。
`layoutList` 自定义条目提供五级模板钩子,按优先级取第一个命中的:
## 二、模板选型建议
| 优先级 | 钩子 | 类型 | 引擎 | 适用场景 |
|--------|------|------|------|---------|
| 1 | `tplFunction` | function | 框架行循环 | 纯 JS 拼 HTML,逻辑重、不想引模板引擎 |
| 2 | `tplLay` | string | laytpl | laytpl 模板字符串,行级作用域 |
| 3 | `tplVue` | string | Vue 模板字符串 | Vue 模板,列表级单实例 |
| 4 | `tplLayId` | string | laytpl | `<script type="text/html">` 节点选择器 |
| 5 | `tplVueId` | string | Vue | `<script type="text/x-template">` 节点选择器 |
`layoutList` 自定义条目提供五种模板钩子,协议定义、优先级与条目写法(对象条目的 name/title/钩子字段语义)见 ulthon-admin-table 第十一章 11.2,此处只给选型结论。
条目写法(对象条目,`name` 必填,`title` 供视图下拉显示,钩子提供一种即生效):
```js
// 出处:extend/base/admin/view/system/admin/index.js:12(官方名片示例)
layoutList: ['table', {name: 'usercard', title: '名片', tplVueId: '#usercardTpl'}, 'card'],
```
**选型建议**:官方两个范例(用户名片、商品卡)用的都是 `tplVueId`,推荐跟随。理由:
**选型结论:跟随官方两个范例(用户名片、商品卡),选 `tplVueId`。** 理由:
- 模板以 HTML 节点形式写在 index.html 里,与页面级 `<style>` 同处一文件,结构清晰
- Vue 模板的 `{{ }}` 原生插值自动 HTML 转义,纯文本字段不用操心转义(laytpl 需要区分 `{{ }}` 转义与 `{{- }}` 原样输出,容易踩坑)
- Vue 模板的 `{{ }}` 原生插值自动 HTML 转义,纯文本字段不用操心转义(laytpl 需要区分 `{{ }}` 转义与 `{{- }}` 原样输出,容易踩坑,详见第三章 laytpl 补充)
- `@error`、`v-if`、`:class` 等指令让图片降级、条件渲染这类需求写起来最短
五种钩子全无且条目名不是 table 时,框架 console.warn 并回退表格形态;模板渲染失败(节点缺失、laytpl 错误、Vue 挂载失败)同样自动回退且不写形态记忆。这些兜底行为详见 ulthon-admin-table 第十一章 11.2。
模板渲染失败的兜底回退由框架提供,机制见 ulthon-admin-table 第十一章 11.2;对模板作者而言,回退本身就是排查信号(见第八章 checklist)。
## 三、渲染上下文契约(自定义模板拿到什么、不能做什么)
**行上下文**(Vue 形态的 `v-for="d in rows"` 里的 `d`):
### 3.1 行上下文
Vue 形态的 `v-for="d in rows"` 里的 `d`:
| 字段 | 说明 |
|------|------|
@@ -67,22 +55,32 @@ layoutList: ['table', {name: 'usercard', title: '名片', tplVueId: '#usercardTp
| `_checked` | 该行是否选中(store 选中集合) |
| `_disabled` | 该行是否禁选 |
**列表上下文**(Vue 形态叫 `list`):`{tableId, rows, page, count, checkedAll}`。注意 laytpl 形态挂在 `d._list` 上,Vue 实例不代理 `_` 前缀键,所以 Vue 形态叫 `list`,裸写 `_list` 会 ReferenceError。
### 3.2 列表上下文
**上下文冻结**:渲染上下文被冻结,处理函数(`@click`、`@error` 等)只读,修改模板数据无效且违约。需要改 DOM 时只操作元素自身样式,不改数据,数据更新一律由框架整容器重建。商品卡图片降级就是标准写法(只藏 img,不碰数据):
`{tableId, rows, page, count, checkedAll}`。laytpl 形态挂在 `d._list` 上,Vue 形态叫 `list`:Vue 实例不代理 `_` 前缀键,裸写 `_list` 会 ReferenceError。
### 3.3 上下文冻结
渲染上下文被冻结,处理函数(`@click`、`@error` 等)只读,修改模板数据无效且违约。需要改 DOM 时只操作元素自身样式,不改数据,数据更新一律由框架整容器重建。商品卡图片降级就是标准写法(只藏 img,不碰数据):
```html
<!-- 出处:extend/base/admin/view/mall/goods/index.html:46-47 -->
<!-- 出处:extend/base/admin/view/mall/goods/index.html(goodscard-cover 图区) -->
<img v-if="d.logo" :src="d.logo" :alt="d.title" class="goodscard-img"
@error="$event.target.style.display = 'none'">
```
**模板节点位置(硬约束)**:`<script type="text/x-template">` 必须写在页面根容器(`.layuimini-container`)之外、Vue 渲染容器之外。节点写进渲染容器内会被模板引擎编译时吞掉。Vue 渲染容器由框架动态创建,元素 id 为 `ua-layout-{tableRenderId}`(默认页面即 `#ua-layout-currentTableRenderId`,取的是 render 实例 id,**不是表格元素 id**,按表格元素 id 写选择器会扑空)。页面结构上,写在根容器 `</div>` 之后即可。
### 3.4 模板节点位置(硬约束)
**行选中委托**:行根元素携带 `data-table-id` + `data-id` 两属性即获得统一行选中委托,点卡片任意空白处切换勾选,零 JS。选中态经 `:class` 高亮回显:
`<script type="text/x-template">` 必须写在页面根容器(`.layuimini-container`)之外、Vue 渲染容器之外。节点写进渲染容器内会被模板引擎编译时吞掉。
渲染容器由框架动态创建(构建与命名机制见 ulthon-admin-table 第十一章 11.1/11.2),定位容器时勿按表格元素 id 推导选择器,按表格元素 id 写会扑空。页面结构上,模板节点写在根容器 `</div>` 之后即可。
### 3.5 行选中委托与豁免名单
行根元素携带 `data-table-id` + `data-id` 两属性即获得统一行选中委托,点卡片任意空白处切换勾选,零 JS。选中态经 `:class` 高亮回显,禁选行整元素不可勾选:
```html
<!-- 出处:extend/base/admin/view/mall/goods/index.html:35-40(usercardTpl 同构) -->
<!-- 出处:extend/base/admin/view/mall/goods/index.html(卡片根元素,usercardTpl 同构) -->
<div class="ua-data-card goodscard"
v-for="d in rows"
:key="d.id"
@@ -91,9 +89,28 @@ layoutList: ['table', {name: 'usercard', title: '名片', tplVueId: '#usercardTp
:class="{'ua-data-card-checked': d._checked, 'ua-data-card-disabled': d._disabled}">
```
委托的豁免名单(点击这些元素不会触发勾选):`a, button, input, select, textarea, label, .layui-form-switch, .layui-form-checkbox, .layui-form-radio`。也就是说按钮点击、switch 切换、checkbox 勾选这些交互天然不误触卡片选中,模板里不需要写任何阻止冒泡的代码。注意委托只在渲染容器内部生效(实现按 `closest('#ua-layout-' + tableId)` 判定),这也是模板节点必须在容器外的另一个原因。
委托的豁免名单(点击这些元素不会触发勾选,实测于 admin-table.js 统一行选中委托段,`grep "统一行选中委托"` 定位):`a, button, input, select, textarea, label, .layui-form-switch, .layui-form-checkbox, .layui-form-radio`。也就是说按钮点击、switch 切换、checkbox 勾选这些交互天然不误触卡片选中,模板里不需要写任何阻止冒泡的代码。委托只在渲染容器内部生效(实现按容器 id closest 判定),这也是模板节点必须在容器外的另一个原因(见 3.4)。
**事件无需自行绑定**:操作按钮(`_operatHtml` 里的 data-open/data-request 属性)与 switch(`_cells.status` 里的 layui 结构)经页面级委托与 form 事件天然生效,switch 失败还会自动回滚重渲。
**事件无需自行绑定**:操作按钮(`_operatHtml` 里的 data-open/data-request 属性)与 switch(`_cells.status` 里的 layui 结构)经页面级委托与 form 事件天然生效,switch 提交失败还会自动回滚重渲。
### 3.6 tplLay 形态补充(非 Vue 路线)
选 laytpl 系钩子(tplLay/tplLayId)时注意语法(layui 2.10.1 内置 laytpl v2.0.0 实测):`{{ d.x }}` 与 `{{= d.x }}` 都会 HTML 转义,`{{- d.x }}` 才是原样输出;`_cells`/`_operatHtml` 是渲染产物 HTML,必须用 `{{- }}`;纯文本用 `{{ }}` 转义更安全;`{{# }}` 写 JS 语句;列表上下文是 `d._list`。最小骨架:
```html
<!-- 页面模板区:渲染容器之外 -->
<script type="text/html" id="myCardTpl">
<div class="my-card">
<h3>{{- d._cells.title }}</h3>
<div>{{- d._operatHtml }}</div>
{{# if (d._checked) { }}<i class="layui-icon layui-icon-ok-circle"></i>{{# } }}
</div>
</script>
```
```js
layoutList: ['table', {name: 'mycard', title: '自定义卡', tplLayId: '#myCardTpl'}],
```
## 四、实战配方:以商品卡为蓝本走一遍
@@ -101,10 +118,10 @@ layoutList: ['table', {name: 'usercard', title: '名片', tplVueId: '#usercardTp
### 第 1 步:index.html 写模板节点与页面级样式
在根容器 `</div>` 之后追加 `<script type="text/x-template">` 节点。骨架部分如下(完整文件见 `extend/base/admin/view/mall/goods/index.html`,节点处的大段规约注释值得通读一遍再动手):
在根容器 `</div>` 之后追加 `<script type="text/x-template">` 节点(位置硬约束见 3.4),同文件追加页面级 `<style>`。骨架要点如下,完整实码见 `extend/base/admin/view/mall/goods/index.html` 的 goodscardTpl 节点与 `.goodscard-*` 样式族(节点处的大段规约注释建议通读一遍再动手):
```html
<!-- 出处:extend/base/admin/view/mall/goods/index.html:33-50(节选,图区部分) -->
<!-- 出处:extend/base/admin/view/mall/goods/index.html(goodscardTpl 节点,图区节选) -->
<script type="text/x-template" id="goodscardTpl">
<div class="goodscard-list">
<div class="ua-data-card goodscard"
@@ -121,7 +138,7 @@ layoutList: ['table', {name: 'usercard', title: '名片', tplVueId: '#usercardTp
<div class="goodscard-status" v-html="d._cells.status"></div>
<i v-if="d._checked" class="layui-icon layui-icon-ok-circle goodscard-check"></i>
</div>
<!-- 主体与底部区块完整结构见源文件 54-75 行 -->
<!-- 主体与底部区块完整结构见源文件 goodscard-body 与 goodscard-footer -->
</div>
</div>
</script>
@@ -130,7 +147,7 @@ layoutList: ['table', {name: 'usercard', title: '名片', tplVueId: '#usercardTp
同文件追加页面级 `<style>`。网格与卡片基础观感部分:
```css
/* 出处:extend/base/admin/view/mall/goods/index.html:85-104(节选) */
/* 出处:extend/base/admin/view/mall/goods/index.html(页面级 style,网格与基础观感节选) */
.goodscard-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
@@ -157,43 +174,41 @@ layoutList: ['table', {name: 'usercard', title: '名片', tplVueId: '#usercardTp
### 第 2 步:index.js 配 layoutList 条目
render 配置块里加一个对象条目,`name` 是形态名(layoutDefault、记忆、下拉定位都用它),`title` 是视图下拉显示文字,`tplVueId` 指向第 1 步的节点 id:
render 配置块里加一个对象条目(字段语义见 ulthon-admin-table 第十一章 11.2),`tplVueId` 指向第 1 步的节点 id:
```js
// 出处:extend/base/admin/view/mall/goods/index.js:13
// 出处:extend/base/admin/view/mall/goods/index.js(render 配置块)
layoutList: ['table', {name: 'goodscard', title: '商品', tplVueId: '#goodscardTpl'}, 'card'],
```
条目数大于 1,工具栏自动出现"视图"按钮;保留 `'card'` 兜底条目可让用户再切回内置卡片,不需要就只留两项。
保留 `'card'` 兜底条目可让用户再切回内置卡片,不需要就只留两项。
### 第 3 步(可选):layoutDefault 设默认形态
两种写法都有官方示范,按需选一:
两种写法都有官方示范,写法语义与记忆/按端/端缺省解析链见 ulthon-admin-table 第十一章 11.2,按需选一:
```js
// 对象式按端取值。出处:extend/base/admin/view/system/admin/index.js:14
// 对象式写法。出处:extend/base/admin/view/system/admin/index.js(render 配置块)
layoutDefault: {pc: 'table', mobile: 'card'},
// 字符串旧写法,双端同值。出处:extend/base/admin/view/mall/goods/index.js:15
// 字符串写法。出处:extend/base/admin/view/mall/goods/index.js(render 配置块)
layoutDefault: 'goodscard',
```
不配则走端缺省:PC 默认表格,手机端缺省取清单里的 `card` 条目(清单无 card 时取 table,再无则首条目)。解析优先级为记忆 > 按端显式 > 端缺省,完整规则见 ulthon-admin-table 第十一章 11.2 三层链。
### 第 4 步:浏览器验证
切换到新形态,按第八章 checklist 过一遍。重点确认:三形态切换零网络请求、模板内开关可切且不误触勾选、图片 404 时占位显示布局不塌、窄屏单列不崩。
切换到新形态,按第八章 checklist 逐项过。重点确认:三形态切换零网络请求、模板内开关可切且不误触勾选、图片 404 时占位显示布局不塌、窄屏单列不崩。
## 五、设计模式库(从两个 v2 模板提炼)
以下模式均来自 usercardTpl 与 goodscardTpl 实码,出处文件已注明,可直接搬。
以下模式均来自 usercardTpl 与 goodscardTpl 实码,出处为文件内选择器锚点,可直接搬。皮肤系列切换全局配色时观感可能随之变化,色值以当前配色系统为准。
| 模式 | 样式要点 | 出处 |
|------|---------|------|
| 头部三件套(头像/标题/开关同行) | flex + `align-items: center` + `gap: 10px`;头像 `flex: none` 固定 48px;姓名/账户上下两行容器 `flex: 1; min-width: 0` 保省略号生效;开关容器 `flex: none; margin-left: auto` 悬浮右侧,不再独占一行 | system/admin/index.html `.usercard-header` 族 |
| 图面悬浮开关 | 容器 `position: absolute; top: 8px; right: 8px` + 半透明白底(`rgba(255,255,255,0.85)`)+ 圆角 8px + 轻投影;内层 `.layui-form-switch { margin: 0 }` 去掉 layui 默认外边距;图区自身 `position: relative` | mall/goods/index.html `.goodscard-status` |
| 价格层级 | 现价强调色大字(`#ff5722`,18px,600 字重),货币符用 `<em>` 缩小到 14px;市场价灰删除线弱化(`#999`,12px,`text-decoration: line-through`);两元素 `align-items: baseline` 对齐 | mall/goods/index.html `.goodscard-price-row` 族 |
| 状态徽标 pill(条件色) | `padding: 2px 8px; border-radius: 10px; font-size: 12px`;条件分支换色:`v-if="d.stock < 20"` 红"缺货"(文字 #ff5722 + 底 `rgba(255,87,34,0.08)`),`v-else` 绿"库存 N"(rgb(22,183,119) + 8% 透明底);阈值与表格列 valueParser 语义对齐 | mall/goods/index.html `.goodscard-stock` 族 + index.js:40-45 |
| 状态徽标 pill(条件色) | `padding: 2px 8px; border-radius: 10px; font-size: 12px`;条件分支换色:`v-if="d.stock < 20"` 红"缺货"(文字 #ff5722 + 底 `rgba(255,87,34,0.08)`),`v-else` 绿"库存 N"(rgb(22,183,119) + 8% 透明底);阈值与表格列 valueParser 语义对齐(goods 页 stock 列 valueParser 同为 20) | mall/goods/index.html `.goodscard-stock` 族 + index.js stock 列 valueParser |
| 底部分隔排布(时间左/操作右) | 容器 flex:时间 `flex: 1; min-width: 0` + 省略号,操作组 `flex: none`;分隔线用 `border-top: 1px solid #eee; padding-top: 10px; margin-top: 10px` | system/admin/index.html `.usercard-footer` |
| 图片 cover + @error 降级 | 占位图标层 `position: absolute` 常驻铺满图区,img `position: relative`(盖住占位,加载中先露占位后覆盖,渐进显示);`object-fit: cover` 撑满裁切不变形;图区用 `aspect-ratio: 4 / 3` 撑住高度,404 隐藏 img 后布局不塌 | mall/goods/index.html `.goodscard-cover`/`.goodscard-img` 族 |
| 头像圆形降级 | 占位层与 img 同位叠放(都 absolute 于 48px 方形容器),都 `border-radius: 50%`;img `@error` 隐藏后露占位图标 | system/admin/index.html `.usercard-avatar` 族 |
@@ -209,22 +224,22 @@ layoutDefault: 'goodscard',
**规则 1:渲染器语义字段用 `_cells` + v-html。** switch、price、image、list 这类列在表格里配了 templet 渲染器,卡片里想要同样的框架语义(switch 可切换、价格带货币符号、状态映射文字)就取 `_cells.field` 并用 `v-html` 输出。`_cells` 是渲染产物 HTML,纯文本插值会显示转义后的标签源码:
```html
<!-- 出处:extend/base/admin/view/system/admin/index.html:47 -->
<!-- 出处:extend/base/admin/view/system/admin/index.html(usercard-status 节点) -->
<div class="usercard-status" v-html="d._cells.status"></div>
```
**规则 2:纯文本直接插值原始字段。** 没配渲染器的字段(title、phone、create_time、销量、价格数字等)`_cells` 里恒为空串,直接 `{{ d.xxx }}` 插值,Vue 自动转义。关联对象注意判空,分类是对象不是字符串:
```html
<!-- 出处:extend/base/admin/view/mall/goods/index.html:55-62(节选) -->
<!-- 出处:extend/base/admin/view/mall/goods/index.html(goodscard-body 节选) -->
<div class="goodscard-title" :title="d.title">{{ d.title }}</div>
<span class="goodscard-cate">{{ d.cate && d.cate.title ? d.cate.title : '未分类' }}</span>
```
**规则 3:特殊呈现绕开渲染器直绑原始字段。** 名片要圆形大头像、商品卡要 4:3 大图,渲染器的内联缩略图样式不合适,`:src` 直绑原始字段,配第 3 章/第 5 章的降级方案:
**规则 3:特殊呈现绕开渲染器直绑原始字段。** 名片要圆形大头像、商品卡要 4:3 大图,渲染器的内联缩略图样式不合适,`:src` 直绑原始字段,配第三章/第五章的降级方案:
```html
<!-- 出处:extend/base/admin/view/system/admin/index.html:39-41 -->
<!-- 出处:extend/base/admin/view/system/admin/index.html(usercard-avatar 头像节点) -->
<img v-if="d.head_img" :src="d.head_img" :alt="d.nickname || d.username"
class="usercard-avatar usercard-avatar-img"
@error="$event.target.style.display = 'none'">
@@ -234,17 +249,17 @@ layoutDefault: 'goodscard',
## 七、坑与红线(全部来自实战台账)
以下每条都是本轮交付中实际踩过或实证过的行为,不是推测。
以下每条都是实际踩过或实证过的行为,不是推测。
**缓存键与验证环境**
- `?t=` 静态缓存键未 bump 时,改完模板验证必须换全新浏览器 profile 或硬刷新,否则加载的是旧 JS/HTML。本轮 Task1 验证两次都靠新 profile 空缓存保障。
- `?t=` 静态缓存键未 bump 时,改完模板验证必须换全新浏览器 profile 或硬刷新,否则加载的是旧 JS/HTML,改了像没改。
- 桌面与手机验证若共用同一浏览器实例,localStorage 按 origin 共享,桌面切换留下的形态记忆会以最高优先级压过手机端缺省形态。跨端断言"默认形态"前必须 `localStorage.clear()` 后刷新。
- 形态记忆键是 `{PAGE_KEY_NAME}_table-layout-default`,按页面前缀隔离(如 `mall.Goods_index_table-layout-default`),互不干扰;但跨页断言时注意子串误伤(`Watch_index` 会匹配到 `RunWatch_index`)。
- 跨页断言记忆时注意子串误伤(`Watch_index` 会匹配到 `RunWatch_index`),过滤条件要含控制器边界。记忆键格式、按页前缀隔离与优先级机制见 ulthon-admin-table 第十一章 11.2。
**切换 API 与记忆的差异**
**切换路径与自动化验收**
- `ua.table.setLayoutForm(id, name)` 只切当前形态,**不写记忆**;只有用户点视图下拉条目才写 `setPageSetting`。用 API 切换后刷新页面会回到记忆形态,这是设计行为不是 bug,自动化验收时别混用两种切换路径。
- 视图下拉与 `ua.table.setLayoutForm` API 是两条切换路径,对形态记忆的行为不同(机制见 ulthon-admin-table 第十一章 11.2)。自动化验收勿混用,混用会把设计行为误判为 bug。
**layui 2.10 专项**
@@ -258,23 +273,23 @@ layoutDefault: 'goodscard',
**Vue 契约**
- 项目内置 Vue 2.6.10(`public/static/plugs/vue-2.6.10/`),运行时编译模式:模板里 `@click` 可用,但处理函数内不得修改模板数据(上下文冻结,改了无效且违约)。DOM 副作用只操作元素自身(如 `@error` 里藏 img)。
- 列表上下文在 Vue 形态叫 `list` 不叫 `_list`,裸写 `_list` 会 ReferenceError(Vue 实例不代理 `_` 前缀键)。
- 列表上下文在 Vue 形态叫 `list` 不叫 `_list`,裸写 `_list` 会 ReferenceError(见 3.2)。
**框架行为边界**
- 操作按钮配置对象自 commit 53ee2e3 起有拷贝保护(渲染函数对配置只读):自定义按钮的 class/method 不会被渲染过程污染,形态反复切换后按钮样式与 URL 拼接保持稳定。修复前的版本存在"切换形态后按钮 class 丢失、URL 滚雪球"的缺陷,老版本升级后若仍见此症状先确认包含 53ee2e3。
- 操作按钮配置对象自 commit 53ee2e3 起有拷贝保护(渲染函数对配置只读):自定义按钮的 class/method 不会被渲染过程污染,形态反复切换后按钮样式与 URL 拼接保持稳定。这是已修复的历史坑:修复前的版本存在"切换形态后按钮 class 丢失、URL 滚雪球"的缺陷,老版本升级后若仍见此症状先确认包含 53ee2e3。
- 形态切换零请求,但 switch 开关切换不是:点击开关会触发 modify 提交加一次列表重查(各 1 个 XHR),这是框架标准链路,验收时勿与"切换零请求"混淆。
- 卡片字段清单是 render 注册时的 cols 快照,搜索列面板运行时隐藏的列不会从卡片消失(两套状态解耦),属已知边界,修复方向已在 pro 仓库立议题跟踪,模板侧无需规避。
- 卡片字段清单是 render 注册时的 cols 快照,搜索列面板运行时隐藏的列不会从卡片消失(两套状态解耦),属已知边界,已立议题跟踪,模板侧无需规避。
## 八、验收 checklist
模板写完,逐项过:
- [ ] **三形态循环零网络请求**:表格、自定义模板、内置 card 来回切,XHR 计数不变(数据取自 store 当页数据)
- [ ] **三形态循环零网络请求**:表格、自定义模板、内置 card 来回切,XHR 计数不变
- [ ] **F5 记忆保持**:通过视图下拉切到自定义形态后刷新,形态保持(说明下拉写入的记忆被正确读取)
- [ ] **手机单列不崩**:390 宽视口下网格单列、无横向滚动(`ua.checkMobile()` 判定需 UA 或 screen 尺寸配合,仅改 viewport 宽度不触发)
- [ ] **手机单列不崩**:390 宽视口下网格单列、无横向滚动(端判定与仿真要点见 ulthon-admin-table 第十一章 11.3)
- [ ] **开关可切且不触发勾选**:点卡片内 switch,POST modify 200,该卡片不出现选中态(无 `ua-data-card-checked` 类、无勾位图标),豁免名单生效
- [ ] **console 零新增错误**:数据固有 404(如测试图片)可豁免,模板相关零报错;若模板渲染失败框架会 console.error 并回退表格,回退本身也是排查信号
- [ ] **console 零新增错误**:数据固有 404(如测试图片)可豁免,模板相关零报错;模板渲染失败时框架会自动回退表格(机制见 ulthon-admin-table 第十一章 11.2),回退本身也是排查信号
- [ ] **交互豁免有效**:点操作按钮(编辑/删除等)打开预期行为且不勾选卡片;点卡片空白处正常切换勾选
- [ ] **(如有图片字段)404 降级**:无图与加载失败都显示占位,卡片高度不塌
@@ -282,6 +297,6 @@ layoutDefault: 'goodscard',
## 相关技能
- [ulthon-admin-table](../ulthon-admin-table/SKILL.md):表格机制与多形态渲染机制原理(第十一章是本技能的契约来源)
- [ulthon-admin-table](../ulthon-admin-table/SKILL.md):表格机制与多形态渲染机制原理(第十一章是本技能的机制前提与契约来源)
- [ulthon-scheme-curd-workflow](../ulthon-scheme-curd-workflow/SKILL.md):本技能定制的列表页骨架从 CURD 生成开始
- [ulthon-page-qa](../ulthon-page-qa/SKILL.md):页面验证 checklist 与浏览器自测方法