feat(view): 商品列表新增商品自定义卡片模板三形态示例

This commit is contained in:
augushong
2026-09-25 10:37:49 +00:00
parent 53ee2e3c40
commit 58053e0280
2 changed files with 197 additions and 1 deletions

View File

@@ -9,4 +9,193 @@
lay-filter="currentTable">
</table>
</div>
</div>
</div>
<!-- 商品卡片形态(layoutList 自定义 Vue 形态,参照用户管理名片官方示例)
硬约束:本节点必须位于页面根容器之外、Vue 渲染容器(#ua-layout-currentTableRenderId,
由框架在表格容器后动态创建)之外,否则会被 Vue 编译吞掉。
上下文契约(admin-table.js buildLayoutContext):
- rows:行上下文数组,d 与表格行数据同构(含原始字段 + _cells/_operatHtml/_checked/_disabled)
- list:列表上下文 {tableId, page, count, checkedAll}(Vue 形态不叫 _list)
- 行根元素携带 data-table-id + data-id 两属性即获得统一行选中委托(点击卡片
切换勾选,按钮/表单控件/layui 美化层自动豁免),选中态经 :class 高亮回显
- _cells/_operatHtml 含渲染产物 HTML,必须用 v-html 输出;纯文本字段用
{{ }} 原生插值(自动转义);处理函数内不得修改渲染上下文数据(冻结契约)
- 事件无需自行绑定:操作按钮与 switch 经页面级委托和 form 事件天然生效
取值通道(对齐官方范例契约):
- status 开关走 _cells(switch 渲染器产物)+ v-html,切换由页面级委托承接
- logo 大图绕开渲染器直绑原始字段(渲染器内联缩略图样式不适合大图);
占位图标层常驻其下,img 加载失败时 @error 仅隐藏 img 露出占位(只操作
DOM,不改模板数据),无 logo 时同样显示占位
- title/cate.title/价格/库存/销量/create_time 为无渲染器纯文本,直接原始字段插值 -->
<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}">
<!-- 头部:商品大图(宽度撑满、固定高裁切不变形),无图或 404 时
降级为占位图标;右上勾位图标按行选中态显示 -->
<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'">
<i v-if="d._checked" class="layui-icon layui-icon-ok-circle goodscard-check"></i>
</div>
<!-- 主体:名称加粗 + 分类 + 价格层级(折扣价红大字、市场价灰删除线)
+ 库存徽标(stock<20 红"缺货",否则绿"库存 N")+ 销量小字
+ 状态开关(_cells.status 为 switch 渲染器输出的原生 checkbox) -->
<div class="goodscard-body">
<div class="goodscard-title" :title="d.title">{{ d.title }}</div>
<div class="goodscard-cate">
<i class="layui-icon layui-icon-tags"></i>
{{ d.cate && d.cate.title ? d.cate.title : '未分类' }}
</div>
<div class="goodscard-price-row">
<span class="goodscard-price">¥{{ d.discount_price }}</span>
<span v-if="parseFloat(d.market_price) > 0" class="goodscard-market-price">¥{{ d.market_price }}</span>
</div>
<div class="goodscard-flag-row">
<span v-if="d.stock < 20" class="goodscard-stock goodscard-stock-out">缺货</span>
<span v-else class="goodscard-stock goodscard-stock-in">库存 {{ d.stock }}</span>
<span class="goodscard-sales">销量 {{ d.sales }}</span>
</div>
<div class="goodscard-status" v-html="d._cells.status"></div>
</div>
<!-- 底部:创建时间 + 操作按钮组(_operatHtml 为操作列渲染产物:
编辑/入库/详情/删除,已按 data-auth-* 裁剪权限),点击行为由
框架页面级委托承接 -->
<div class="ua-data-card-footer goodscard-footer">
<span class="ua-data-card-time"><i class="layui-icon layui-icon-time"></i> {{ d.create_time }}</span>
<div class="ua-data-card-operat" v-html="d._operatHtml"></div>
</div>
</div>
</div>
</script>
<!-- 商品卡片形态页面级样式(示例边界:网格布局、大图封面、价格/徽标为本页
自建,卡片基础观感——边框/白底/hover/选中描边/禁用/操作按钮排布——复用
public.css 的 .ua-data-card 族,不修改公共样式) -->
<style>
.goodscard-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 12px;
}
.goodscard {
padding: 0;
overflow: hidden;
}
.goodscard-cover {
position: relative;
height: 140px;
background-color: #f6f6f6;
border-bottom: 1px solid #f2f2f2;
}
.goodscard-img-empty {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
color: #c2c2c2;
font-size: 32px;
}
.goodscard-img {
position: relative;
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.goodscard-check {
position: absolute;
top: 6px;
right: 6px;
color: rgb(22, 183, 119);
font-size: 22px;
background-color: #fff;
border-radius: 50%;
}
.goodscard-body {
flex: 1;
padding: 8px 12px 0;
}
.goodscard-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: bold;
line-height: 22px;
}
.goodscard-cate {
margin-top: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #999;
font-size: 12px;
line-height: 18px;
}
.goodscard-cate .layui-icon {
font-size: 12px;
}
.goodscard-price-row {
display: flex;
align-items: baseline;
margin-top: 6px;
}
.goodscard-price {
color: #ff5722;
font-size: 18px;
font-weight: bold;
}
.goodscard-market-price {
margin-left: 8px;
color: #c2c2c2;
font-size: 12px;
text-decoration: line-through;
}
.goodscard-flag-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 6px;
}
.goodscard-stock {
padding: 0 6px;
border-radius: 2px;
font-size: 12px;
line-height: 18px;
}
.goodscard-stock-out {
color: #ff5722;
background-color: rgba(255, 87, 34, 0.1);
}
.goodscard-stock-in {
color: rgb(22, 183, 119);
background-color: rgba(22, 183, 119, 0.1);
}
.goodscard-sales {
color: #999;
font-size: 12px;
}
.goodscard-status {
margin-top: 6px;
text-align: right;
}
.goodscard-footer {
margin-top: 8px;
padding: 8px 12px 10px;
}
@media (max-width: 768px) {
.goodscard-list {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -4,6 +4,13 @@ $(function () {
ua.table.render({
init: init,
totalRow: true,
// 多形态视图:table + 商品卡(Vue 自定义模板)+ 内置 card 三形态,
// 参照用户管理页官方示例(system/admin)。tplVueId 指向 index.html 中的
// <script type="text/x-template" id="goodscardTpl"> 节点(须位于渲染容器
// 之外),模板上下文契约见该节点处注释。layoutList 条目 >1 时工具栏自动
// 出现"视图"切换按钮;受控是默认数据流,controlled 缺省即受控,视图切换
// 经 store 当页数据完成,零网络请求。
layoutList: ['table', {name: 'goodscard', title: '商品', tplVueId: '#goodscardTpl'}, 'card'],
cols: [[
{ type: "checkbox" },
{ field: 'id', width: 80, title: 'ID', totalRowText: '合计:' },