Files
ulthon_admin/.agents/skills/ulthon-card-template/SKILL.md
augushong faf00f324f feat(table): 列筛选显隐按页记忆,卡片设置入口移至左侧工具栏
- 列筛选面板勾选状态按页 localStorage 记忆,刷新后表格/卡片两侧一致恢复
- 卡片设置按钮由右侧图标区移至左侧操作按钮区,面板向下展开不再超出视口
- 入口改按钮直绑点击并抽出 toggleCardSettingPanel 复用,不再走 defaultToolbar 注入
2026-10-05 02:12:23 +00:00

22 KiB
Raw Blame History

name, description
name description
ulthon-card-template 自定义卡片/模板形态创作指南:面向想让列表页变好看、为自己的列表页定制专属卡片形态的下游开发者。涵盖适用判断(内置 card 兜底 vs 定制时机、锁定纯表格)、五级模板协议选型建议(官方范例为何都选 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),两者都是框架分发、可直接对照的官方范例。

机制前提:多形态机制(受控数据流为什么零请求、layoutList 五级协议与回退、双端自动注入、layoutDefault 三层解析链、模板渲染容器的构建与命名)统一由 ulthon-admin-table 技能第十一章定义,本技能不复述。本技能回答的是"怎么动手写"。

一、适用判断:先想清楚要不要定制

不定制就够用的情况(多数页面属于这类):ua.table.render 不配 layoutList 时,框架自动注入"表格 + 内置 card"双形态,工具栏自动出现"视图"按钮,字段按 label:value 紧凑网格渲染,零配置零模板。内置 card 的信息密度对管理后台的字段列表场景通常已经足够,够用就别写模板。

值得定制的情况:

  • 需要语义化排版:头部大图 + 价格层级 + 状态徽标这类商品/内容展示形态,内置 card 的 label:value 网格表达不了
  • 需要品牌感:自定义配色、圆角、卡片结构(头部三件套、悬浮开关等)
  • 需要字段编排控制:哪些字段大字强调、哪些弱化、哪些组合进徽标,与表格列语义不同

锁定纯表格的出口:页面明确不需要任何卡片形态时(例如纯运维日志类页面),配单条目清单即可:

layoutList: ['table'],

单条目锁定的语义与判断逻辑见 ulthon-admin-table 第十一章 11.2。

一个例外提醒:若页面显式传 controlled: false 走逃生口(依赖引擎 url 模式行为的深度定制页,如自定义 parseData),不要配 layoutList。是否属于这类页面的判断见 ulthon-admin-table 第十一章 11.1。

二、模板选型建议

layoutList 自定义条目提供五种模板钩子,协议定义、优先级与条目写法(对象条目的 name/title/钩子字段语义)见 ulthon-admin-table 第十一章 11.2,此处只给选型结论。

选型结论:跟随官方两个范例(用户名片、商品卡),选 tplVueId。 理由:

  • 模板以 HTML 节点形式写在 index.html 里,与页面级 <style> 同处一文件,结构清晰
  • Vue 模板的 {{ }} 原生插值自动 HTML 转义,纯文本字段不用操心转义(laytpl 需要区分 {{ }} 转义与 {{- }} 原样输出,容易踩坑,详见第三章 laytpl 补充)
  • @error、v-if、:class 等指令让图片降级、条件渲染这类需求写起来最短

模板渲染失败的兜底回退由框架提供,机制见 ulthon-admin-table 第十一章 11.2;对模板作者而言,回退本身就是排查信号(见第八章 checklist)。

三、渲染上下文契约(自定义模板拿到什么、不能做什么)

3.1 行上下文

Vue 形态的 v-for="d in rows" 里的 d:

字段 说明
...row 行原始字段全部可用(username、title、price、create_time 等)
_cells 对象,field 到该列 templet 渲染产物 HTML 的映射。只有配了渲染器的列才有内容;纯文本列恒为空串,勿引用,直接用原始字段;隐藏列(col.hide)不产出对应键
_operatHtml 操作列渲染产物(编辑/删除等按钮 HTML,已按当前登录者权限裁剪)
_checked 该行是否选中(store 选中集合)
_disabled 该行是否禁选

3.2 列表上下文

{tableId, rows, page, count, checkedAll}。laytpl 形态挂在 d._list 上,Vue 形态叫 list:Vue 实例不代理 _ 前缀键,裸写 _list 会 ReferenceError。

3.3 上下文冻结

渲染上下文被冻结,处理函数(@click、@error 等)只读,修改模板数据无效且违约。需要改 DOM 时只操作元素自身样式,不改数据,数据更新一律由框架整容器重建。商品卡图片降级就是标准写法(只藏 img,不碰数据):

<!-- 出处: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'">

3.4 模板节点位置(硬约束)

<script type="text/x-template"> 必须写在页面根容器(.layuimini-container)之外、Vue 渲染容器之外。节点写进渲染容器内会被模板引擎编译时吞掉。

渲染容器由框架动态创建(构建与命名机制见 ulthon-admin-table 第十一章 11.1/11.2),定位容器时勿按表格元素 id 推导选择器,按表格元素 id 写会扑空。页面结构上,模板节点写在根容器 </div> 之后即可。

3.5 行选中委托与豁免名单

点行内 .ua-data-card-check 勾位元素切换勾选,零 JS;行根元素仍需携带 data-table-id + data-id 两属性(委托按行根属性定位行),且勾位元素必须写在行内(委托按该类判定点击意图,行内没有勾位元素则该行无法勾选)。选中态经 :class 高亮回显,禁选行整元素不可勾选:

<!-- 出处:extend/base/admin/view/mall/goods/index.html(卡片根元素,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}">

委托的豁免名单(点击这些元素不会触发勾选,实测于 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 提交失败还会自动回滚重渲。

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。最小骨架:

<!-- 页面模板区:渲染容器之外 -->
<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>
layoutList: ['table', {name: 'mycard', title: '自定义卡', tplLayId: '#myCardTpl'}],

四、实战配方:以商品卡为蓝本走一遍

目标效果:商品列表增加"商品"形态,图区大图 + 悬浮状态开关 + 价格层级 + 双色库存徽标 + 底部时间/操作排布。四步完成。

第 1 步:index.html 写模板节点与页面级样式

在根容器 </div> 之后追加 <script type="text/x-template"> 节点(位置硬约束见 3.4),同文件追加页面级 <style>。骨架要点如下,完整实码见 extend/base/admin/view/mall/goods/index.html 的 goodscardTpl 节点与 .goodscard-* 样式族(节点处的大段规约注释建议通读一遍再动手):

<!-- 出处: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"
             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 class="layui-icon layui-icon-ok-circle goodscard-check ua-data-card-check"></i>
            </div>
            <!-- 主体与底部区块完整结构见源文件 goodscard-body 与 goodscard-footer -->
        </div>
    </div>
</script>

同文件追加页面级 <style>。网格与卡片基础观感部分:

/* 出处:extend/base/admin/view/mall/goods/index.html(页面级 style,网格与基础观感节选) */
.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 配置块里加一个对象条目(字段语义见 ulthon-admin-table 第十一章 11.2),tplVueId 指向第 1 步的节点 id:

// 出处:extend/base/admin/view/mall/goods/index.js(render 配置块)
layoutList: ['table', {name: 'goodscard', title: '商品', tplVueId: '#goodscardTpl'}, 'card'],

保留 'card' 兜底条目可让用户再切回内置卡片,不需要就只留两项。

第 3 步(可选):layoutDefault 设默认形态

两种写法都有官方示范,写法语义与记忆/按端/端缺省解析链见 ulthon-admin-table 第十一章 11.2,按需选一:

// 对象式写法。出处:extend/base/admin/view/system/admin/index.js(render 配置块)
layoutDefault: {pc: 'table', mobile: 'card'},

// 字符串写法。出处:extend/base/admin/view/mall/goods/index.js(render 配置块)
layoutDefault: 'goodscard',

第 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 语义对齐(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 族
响应式单列 @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,纯文本插值会显示转义后的标签源码:

<!-- 出处: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 自动转义。关联对象注意判空,分类是对象不是字符串:

<!-- 出处: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 直绑原始字段,配第三章/第五章的降级方案:

<!-- 出处: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'">

数据类型细节(商品卡实测,来自接口实返回结构):价格字段是字符串("2.00"),市场价 0 不渲染删除线用 parseFloat(d.market_price) > 0 判断(parseFloat 在 Vue 2 全局白名单内,模板里可直接用);时间字段 create_time 由模型获取器预先格式化,直接插值即可。

七、坑与红线(全部来自实战台账)

以下每条都是实际踩过或实证过的行为,不是推测。

缓存键与验证环境

  • ?t= 静态缓存键未 bump 时,改完模板验证必须换全新浏览器 profile 或硬刷新,否则加载的是旧 JS/HTML,改了像没改。
  • 桌面与手机验证若共用同一浏览器实例,localStorage 按 origin 共享,桌面切换留下的形态记忆会以最高优先级压过手机端缺省形态。跨端断言"默认形态"前必须 localStorage.clear() 后刷新。
  • 跨页断言记忆时注意子串误伤(Watch_index 会匹配到 RunWatch_index),过滤条件要含控制器边界。记忆键格式、按页前缀隔离与优先级机制见 ulthon-admin-table 第十一章 11.2。

切换路径与自动化验收

  • 视图下拉与 ua.table.setLayoutForm API 是两条切换路径,对形态记忆的行为不同(机制见 ulthon-admin-table 第十一章 11.2)。自动化验收勿混用,混用会把设计行为误判为 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(见 3.2)。

框架行为边界

  • 操作按钮配置对象自 commit 53ee2e3 起有拷贝保护(渲染函数对配置只读):自定义按钮的 class/method 不会被渲染过程污染,形态反复切换后按钮样式与 URL 拼接保持稳定。这是已修复的历史坑:修复前的版本存在"切换形态后按钮 class 丢失、URL 滚雪球"的缺陷,老版本升级后若仍见此症状先确认包含 53ee2e3。
  • 形态切换零请求,但 switch 开关切换不是:点击开关会触发 modify 提交加一次列表重查(各 1 个 XHR),这是框架标准链路,验收时勿与"切换零请求"混淆。
  • 卡片字段清单实时跟随列显隐:列筛选面板隐藏/恢复列后,卡片形态自动重渲(colToggled 监听 + store 当页数据零请求重渲),隐藏列不产出 _cells 对应键,模板侧无需规避。显隐状态按页 localStorage 记忆,刷新保持。

八、验收 checklist

模板写完,逐项过:

  • 三形态循环零网络请求:表格、自定义模板、内置 card 来回切,XHR 计数不变
  • F5 记忆保持:通过视图下拉切到自定义形态后刷新,形态保持(说明下拉写入的记忆被正确读取)
  • 手机单列不崩:390 宽视口下网格单列、无横向滚动(端判定与仿真要点见 ulthon-admin-table 第十一章 11.3)
  • 开关可切且不触发勾选:点卡片内 switch,POST modify 200,该卡片不出现选中态(无 ua-data-card-checked 类、勾位图标保持灰色未选中态),豁免名单生效
  • console 零新增错误:数据固有 404(如测试图片)可豁免,模板相关零报错;模板渲染失败时框架会自动回退表格(机制见 ulthon-admin-table 第十一章 11.2),回退本身也是排查信号
  • 交互豁免有效:点操作按钮(编辑/删除等)打开预期行为且不勾选卡片;点勾位图标正常切换勾选,点卡片正文空白处不触发勾选
  • (如有图片字段)404 降级:无图与加载失败都显示占位,卡片高度不塌

验证工具建议:命令行接口冒烟用 ulthon-tools-http-call;页面断言(形态、XHR、豁免)用浏览器实测,具体断言选择器与仿真要点见 ulthon-page-qa。

相关技能