Files
ulthon_admin/.agents/skills/ulthon-card-template/SKILL.md

288 lines
22 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-card-template"
description: "自定义卡片/模板形态动手配方:面向用自己的列表页定制专属卡片形态的下游开发者。涵盖适用判断、layoutList 五级模板协议选型(tplFunction/tplLay/tplVue/tplLayId/tplVueId)、渲染上下文契约(_cells/_operatHtml/_checked/_disabled 与上下文冻结)、以商品卡为蓝本的 step-by-step 配方、设计模式库(头部三件套/图面悬浮开关/价格层级/状态徽标/底部分隔/图片降级/响应式/统一设计语言)、取值通道三规则、坑与红线、验收 checklist。需要定制卡片/自定义卡片模板/卡片模板/商品卡/名片/多形态模板/layoutList 自定义条目/tplVueId,或排查自定义模板不渲染、点卡片误勾选、模板数据被改无效时调用。机制原理(受控数据流、五级协议引擎、双端注入、layoutDefault 三层链)见 ulthon-admin-table 技能第十一章,本技能定位是动手配方,不复述机制。"
---
# 自定义卡片模板定制指南(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) 技能第十一章。本技能回答的是"怎么动手写"。
## 一、适用判断:先想清楚要不要定制
**不定制就够用的情况(多数页面属于这类)**:`ua.table.render` 不配 `layoutList` 时,框架自动注入"表格 + 内置 card"双形态,工具栏自动出现"视图"按钮,字段按 label:value 紧凑网格渲染,零配置零模板。内置 card 的信息密度对管理后台的字段列表场景通常已经足够,**够用就别写模板**。
**值得定制的情况**:
- 需要语义化排版:头部大图 + 价格层级 + 状态徽标这类商品/内容展示形态,内置 card 的 label:value 网格表达不了
- 需要品牌感:自定义配色、圆角、卡片结构(头部三件套、悬浮开关等)
- 需要字段编排控制:哪些字段大字强调、哪些弱化、哪些组合进徽标,与表格列语义不同
**锁定纯表格的出口**:如果页面明确不需要任何卡片形态(例如纯运维日志类页面),配单条目清单即可,工具栏不会出现视图按钮:
```js
// 场景出处:本轮 QA 实证(table-card-global-enabled 计划 Task9 场景 2,host 页临时配置验证)
layoutList: ['table'],
```
**一个例外提醒**:显式传 `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">` 节点选择器 |
条目写法(对象条目,`name` 必填,`title` 供视图下拉显示,钩子提供一种即生效):
```js
// 出处:extend/base/admin/view/system/admin/index.js:12(官方名片示例)
layoutList: ['table', {name: 'usercard', title: '名片', tplVueId: '#usercardTpl'}, 'card'],
```
**选型建议**:官方两个范例(用户名片、商品卡)用的都是 `tplVueId`,推荐跟随。理由:
- 模板以 HTML 节点形式写在 index.html 里,与页面级 `<style>` 同处一文件,结构清晰
- Vue 模板的 `{{ }}` 原生插值自动 HTML 转义,纯文本字段不用操心转义(laytpl 需要区分 `{{ }}` 转义与 `{{- }}` 原样输出,容易踩坑)
- `@error`、`v-if`、`:class` 等指令让图片降级、条件渲染这类需求写起来最短
五种钩子全无且条目名不是 table 时,框架 console.warn 并回退表格形态;模板渲染失败(节点缺失、laytpl 错误、Vue 挂载失败)同样自动回退且不写形态记忆。这些兜底行为详见 ulthon-admin-table 第十一章 11.2。
## 三、渲染上下文契约(自定义模板拿到什么、不能做什么)
**行上下文**(Vue 形态的 `v-for="d in rows"` 里的 `d`):
| 字段 | 说明 |
|------|------|
| `...row` | 行原始字段全部可用(username、title、price、create_time 等) |
| `_cells` | 对象,field 到该列 templet 渲染产物 HTML 的映射。只有配了渲染器的列才有内容;纯文本列恒为空串,**勿引用,直接用原始字段** |
| `_operatHtml` | 操作列渲染产物(编辑/删除等按钮 HTML,已按 `data-auth-*` 裁剪权限) |
| `_checked` | 该行是否选中(store 选中集合) |
| `_disabled` | 该行是否禁选 |
**列表上下文**(Vue 形态叫 `list`):`{tableId, rows, page, count, checkedAll}`。注意 laytpl 形态挂在 `d._list` 上,Vue 实例不代理 `_` 前缀键,所以 Vue 形态叫 `list`,裸写 `_list` 会 ReferenceError。
**上下文冻结**:渲染上下文被冻结,处理函数(`@click`、`@error` 等)只读,修改模板数据无效且违约。需要改 DOM 时只操作元素自身样式,不改数据,数据更新一律由框架整容器重建。商品卡图片降级就是标准写法(只藏 img,不碰数据):
```html
<!-- 出处:extend/base/admin/view/mall/goods/index.html:46-47 -->
<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>` 之后即可。
**行选中委托**:行根元素携带 `data-table-id` + `data-id` 两属性即获得统一行选中委托,点卡片任意空白处切换勾选,零 JS。选中态经 `:class` 高亮回显:
```html
<!-- 出处:extend/base/admin/view/mall/goods/index.html:35-40(usercardTpl 同构) -->
<div class="ua-data-card goodscard"
v-for="d in rows"
:key="d.id"
:data-id="d.id"
:data-table-id="list.tableId"
: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)` 判定),这也是模板节点必须在容器外的另一个原因。
**事件无需自行绑定**:操作按钮(`_operatHtml` 里的 data-open/data-request 属性)与 switch(`_cells.status` 里的 layui 结构)经页面级委托与 form 事件天然生效,switch 失败还会自动回滚重渲。
## 四、实战配方:以商品卡为蓝本走一遍
目标效果:商品列表增加"商品"形态,图区大图 + 悬浮状态开关 + 价格层级 + 双色库存徽标 + 底部时间/操作排布。四步完成。
### 第 1 步:index.html 写模板节点与页面级样式
在根容器 `</div>` 之后追加 `<script type="text/x-template">` 节点。骨架部分如下(完整文件见 `extend/base/admin/view/mall/goods/index.html`,节点处的大段规约注释值得通读一遍再动手):
```html
<!-- 出处:extend/base/admin/view/mall/goods/index.html:33-50(节选,图区部分) -->
<script type="text/x-template" id="goodscardTpl">
<div class="goodscard-list">
<div class="ua-data-card goodscard"
v-for="d in rows"
:key="d.id"
:data-id="d.id"
:data-table-id="list.tableId"
:class="{'ua-data-card-checked': d._checked, 'ua-data-card-disabled': d._disabled}">
<!-- 图区:4:3 大图 + 占位降级 + 悬浮开关 + 勾位图标 -->
<div class="goodscard-cover">
<span class="goodscard-img-empty" title="未设置图片"><i class="layui-icon layui-icon-picture"></i></span>
<img v-if="d.logo" :src="d.logo" :alt="d.title" class="goodscard-img"
@error="$event.target.style.display = 'none'">
<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 行 -->
</div>
</div>
</script>
```
同文件追加页面级 `<style>`。网格与卡片基础观感部分:
```css
/* 出处:extend/base/admin/view/mall/goods/index.html:85-104(节选) */
.goodscard-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
gap: 12px;
}
.goodscard {
padding: 0;
overflow: hidden;
border: 1px solid #eee;
border-radius: 8px;
}
.goodscard:hover {
border-color: #eee;
box-shadow: 0 2px 12px rgba(0, 0, 0, .08);
}
.goodscard.ua-data-card-checked {
border-color: #1e9fff;
box-shadow: inset 0 0 0 1px #1e9fff;
background-color: rgba(30, 159, 255, 0.05);
}
```
样式边界约定:卡片基础观感(圆角 8px、#eee 边框、hover 投影、选中主题蓝描边浅底)按统一设计语言在本页覆盖,公共 `.ua-data-card` 族样式(public.css)不动;操作按钮排布直接复用 `.ua-data-card-footer` / `.ua-data-card-operat` / `.ua-data-card-time` 这些公共类,本页只调外间距。
### 第 2 步:index.js 配 layoutList 条目
render 配置块里加一个对象条目,`name` 是形态名(layoutDefault、记忆、下拉定位都用它),`title` 是视图下拉显示文字,`tplVueId` 指向第 1 步的节点 id:
```js
// 出处:extend/base/admin/view/mall/goods/index.js:13
layoutList: ['table', {name: 'goodscard', title: '商品', tplVueId: '#goodscardTpl'}, 'card'],
```
条目数大于 1,工具栏自动出现"视图"按钮;保留 `'card'` 兜底条目可让用户再切回内置卡片,不需要就只留两项。
### 第 3 步(可选):layoutDefault 设默认形态
两种写法都有官方示范,按需选一:
```js
// 对象式按端取值。出处:extend/base/admin/view/system/admin/index.js:14
layoutDefault: {pc: 'table', mobile: 'card'},
// 字符串旧写法,双端同值。出处:extend/base/admin/view/mall/goods/index.js:15
layoutDefault: 'goodscard',
```
不配则走端缺省:PC 默认表格,手机端缺省取清单里的 `card` 条目(清单无 card 时取 table,再无则首条目)。解析优先级为记忆 > 按端显式 > 端缺省,完整规则见 ulthon-admin-table 第十一章 11.2 三层链。
### 第 4 步:浏览器验证
切换到新形态,按第八章 checklist 过一遍。重点确认:三形态切换零网络请求、模板内开关可切且不误触勾选、图片 404 时占位显示布局不塌、窄屏单列不崩。
## 五、设计模式库(从两个 v2 模板提炼)
以下模式均来自 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 |
| 底部分隔排布(时间左/操作右) | 容器 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` 族 |
| 响应式单列 | `@media (max-width: 768px)` 下网格改 `grid-template-columns: 1fr`,窄屏不崩 | 两个模板文件尾部各有一段 |
| 统一设计语言基线 | 圆角 8px、边框 1px #eee、hover 投影 `0 2px 12px rgba(0,0,0,.08)`、选中态主题色描边(`box-shadow: inset 0 0 0 1px #1e9fff` 或同值 border)+ 浅底(`rgba(30,159,255,0.05)` 或 `#f2f9ff`);公共 `.ua-data-card` 族不动,只做页面级覆盖 | 两个模板的卡片根类样式 |
设计语言基线的意义:用户在"名片/商品/内置卡片"之间切换时观感一致,选中态、hover 反馈不跳变。新模板建议沿用这套基线再叠加业务样式。
## 六、取值通道三规则
一个字段到底用 `_cells` 还是原始字段,按呈现需求分三条路:
**规则 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 -->
<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(节选) -->
<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 章的降级方案:
```html
<!-- 出处:extend/base/admin/view/system/admin/index.html:39-41 -->
<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'">
```
数据类型细节(商品卡实测,来自接口实返回结构):价格字段是字符串("2.00"),市场价 0 不渲染删除线用 `parseFloat(d.market_price) > 0` 判断(parseFloat 在 Vue 2 全局白名单内,模板里可直接用);时间字段 `create_time` 由模型获取器预先格式化,直接插值即可。
## 七、坑与红线(全部来自实战台账)
以下每条都是本轮交付中实际踩过或实证过的行为,不是推测。
**缓存键与验证环境**
- `?t=` 静态缓存键未 bump 时,改完模板验证必须换全新浏览器 profile 或硬刷新,否则加载的是旧 JS/HTML。本轮 Task1 验证两次都靠新 profile 空缓存保障。
- 桌面与手机验证若共用同一浏览器实例,localStorage 按 origin 共享,桌面切换留下的形态记忆会以最高优先级压过手机端缺省形态。跨端断言"默认形态"前必须 `localStorage.clear()` 后刷新。
- 形态记忆键是 `{PAGE_KEY_NAME}_table-layout-default`,按页面前缀隔离(如 `mall.Goods_index_table-layout-default`),互不干扰;但跨页断言时注意子串误伤(`Watch_index` 会匹配到 `RunWatch_index`)。
**切换 API 与记忆的差异**
- `ua.table.setLayoutForm(id, name)` 只切当前形态,**不写记忆**;只有用户点视图下拉条目才写 `setPageSetting`。用 API 切换后刷新页面会回到记忆形态,这是设计行为不是 bug,自动化验收时别混用两种切换路径。
**layui 2.10 专项**
- switch 开启态断言查 `input[type=checkbox].checked`,不要查 `.layui-form-onswitch` 类:新版 layui 视觉由 `:checked` 伪类 CSS 驱动,DOM 上没有 onswitch 类,查类会得出与视觉相反的结论。
**图片与布局**
- 图片 404 降级必须保布局不塌:占位层常驻 + `@error` 只藏 img + 图区 `aspect-ratio`(或固定高)撑住。二选一 `v-if` 写法在加载失败瞬间容器高度塌陷。
- 加载中会先露占位再被 img 覆盖,属渐进显示,可接受;img 不加 `position: relative` 会一直被 absolute 占位层盖住。
**Vue 契约**
- 项目内置 Vue 2.6.10(`public/static/plugs/vue-2.6.10/`),运行时编译模式:模板里 `@click` 可用,但处理函数内不得修改模板数据(上下文冻结,改了无效且违约)。DOM 副作用只操作元素自身(如 `@error` 里藏 img)。
- 列表上下文在 Vue 形态叫 `list` 不叫 `_list`,裸写 `_list` 会 ReferenceError(Vue 实例不代理 `_` 前缀键)。
**框架行为边界**
- 操作按钮配置对象自 commit 53ee2e3 起有拷贝保护(渲染函数对配置只读):自定义按钮的 class/method 不会被渲染过程污染,形态反复切换后按钮样式与 URL 拼接保持稳定。修复前的版本存在"切换形态后按钮 class 丢失、URL 滚雪球"的缺陷,老版本升级后若仍见此症状先确认包含 53ee2e3。
- 形态切换零请求,但 switch 开关切换不是:点击开关会触发 modify 提交加一次列表重查(各 1 个 XHR),这是框架标准链路,验收时勿与"切换零请求"混淆。
- 卡片字段清单是 render 注册时的 cols 快照,搜索列面板运行时隐藏的列不会从卡片消失(两套状态解耦),属已知边界,修复方向已在 pro 仓库立议题跟踪,模板侧无需规避。
## 八、验收 checklist
模板写完,逐项过:
- [ ] **三形态循环零网络请求**:表格、自定义模板、内置 card 来回切,XHR 计数不变(数据取自 store 当页数据)
- [ ] **F5 记忆保持**:通过视图下拉切到自定义形态后刷新,形态保持(说明下拉写入的记忆被正确读取)
- [ ] **手机单列不崩**:390 宽视口下网格单列、无横向滚动(`ua.checkMobile()` 判定需 UA 或 screen 尺寸配合,仅改 viewport 宽度不触发)
- [ ] **开关可切且不触发勾选**:点卡片内 switch,POST modify 200,该卡片不出现选中态(无 `ua-data-card-checked` 类、无勾位图标),豁免名单生效
- [ ] **console 零新增错误**:数据固有 404(如测试图片)可豁免,模板相关零报错;若模板渲染失败框架会 console.error 并回退表格,回退本身也是排查信号
- [ ] **交互豁免有效**:点操作按钮(编辑/删除等)打开预期行为且不勾选卡片;点卡片空白处正常切换勾选
- [ ] **(如有图片字段)404 降级**:无图与加载失败都显示占位,卡片高度不塌
验证工具建议:命令行接口冒烟用 [ulthon-tools-http-call](../ulthon-tools-http-call/SKILL.md);页面断言(形态、XHR、豁免)用浏览器实测,具体断言选择器与仿真要点见 [ulthon-page-qa](../ulthon-page-qa/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 与浏览器自测方法