Files
ulthon_admin/extend/base/admin/view/mall/goods/index.html
augushong 45bc4a7f43 refactor(curd): 移除生成模板与框架页面 data-auth 声明(含 uploadfile 权限映射回归本页节点的行为变化说明)
- CURD 生成模板 index.code 删除 6 个 data-auth 属性行,生成页面零权限声明
- extend/base 13 个列表页 index.html 清理全部 data-auth-* 声明(含注释文本同步改写)
- system/host/index.js 的 set-master 按钮 auth 改写为全节点 'system.host/setMaster'
  (驼峰动作段逐字保留,parseNodeStr 仅 snake 化控制器段);system/auth 的
  assignUsers 解析后与注册节点逐字相等,无需改写
- 行为变化:system/uploadfile 页原 data-auth-add/edit/delete 误映射 system.menu/*
  (上游模板复制残留),删除后按钮按本页 system.uploadfile/* 节点判定——该页
  控制器仅注册 delete 动作,非超管用户的添加按钮从此正确隐藏(现状是
  有 system.menu/add 权限即显示但点击被后端拒绝),data-auth-edit 为死属性
  (本页无编辑按钮)
- VersionBase::VERSION v2.7.0 → v2.8.0,使 admin-core.js 静态引用缓存键
  (?t=get_site_version_key())失效,防下游升级后旧缓存 js 叠加新布局导致
  非超管按钮全隐
2026-10-04 01:31:36 +00:00

237 lines
9.0 KiB
PHP
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.

<div class="layuimini-container">
<div class="layuimini-main">
<table id="currentTable" class="layui-table layui-hide"
lay-filter="currentTable">
</table>
</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,悬浮于图面右上角
(absolute 半透明白底小容器),不占正文行;切换由页面级委托承接
(.layui-form-switch 美化层在豁免名单内),容器只调外间距不改 DOM 结构
- 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}">
<!-- 图区:4:3 商品大图(撑满裁切不变形、圆角随卡片),无图或 404 时
降级为占位图标;右上角悬浮状态开关(半透明白底小容器),
左上角勾位图标常显(.ua-data-card-check:未选中灰可点、选中变绿) -->
<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>
<!-- 主体:名称(600 字重、最多两行截断)+ 分类徽标与销量同排 + 价格
层级(折扣价红大字、市场价灰删除线)+ 库存徽标(stock<20 红
"缺货",否则绿"库存 N") -->
<div class="goodscard-body">
<div class="goodscard-title" :title="d.title">{{ d.title }}</div>
<div class="goodscard-meta">
<span class="goodscard-cate">{{ d.cate && d.cate.title ? d.cate.title : '未分类' }}</span>
<span class="goodscard-sales">销量 {{ d.sales }}</span>
</div>
<div class="goodscard-price-row">
<span class="goodscard-price"><em>¥</em>{{ d.discount_price }}</span>
<span v-if="parseFloat(d.market_price) > 0" class="goodscard-market-price">¥{{ d.market_price }}</span>
</div>
<div class="goodscard-stock-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>
</div>
</div>
<!-- 底部:创建时间 + 操作按钮组(_operatHtml 为操作列渲染产物:
编辑/入库/详情/删除,已按当前登录者权限裁剪),点击行为由
框架页面级委托承接 -->
<div class="ua-data-card-footer goodscard-footer">
<span class="ua-data-card-time goodscard-time">{{ d.create_time }}</span>
<div class="ua-data-card-operat" v-html="d._operatHtml"></div>
</div>
</div>
</div>
</script>
<!-- 商品卡片形态页面级样式(示例边界:网格布局、大图封面、悬浮开关、价格/
徽标为本页自建,卡片基础观感按统一设计语言在本页覆盖——圆角 8px、边框
#eee、hover 投影、选中主题蓝描边浅蓝底;操作按钮排布仍复用 public.css
的 .ua-data-card 族,不修改公共样式) -->
<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);
}
.goodscard-cover {
position: relative;
aspect-ratio: 4 / 3;
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: #d5d5d5;
font-size: 36px;
}
.goodscard-img {
position: relative;
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.goodscard-status {
position: absolute;
top: 8px;
right: 8px;
z-index: 2;
padding: 2px 6px;
background-color: rgba(255, 255, 255, 0.85);
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
}
.goodscard-status .layui-form-switch {
margin: 0;
}
.goodscard-check {
position: absolute;
top: 8px;
left: 8px;
margin-left: 0;
font-size: 22px;
background-color: #fff;
border-radius: 50%;
}
.goodscard-body {
flex: 1;
padding: 10px 12px 12px;
}
.goodscard-title {
display: -webkit-box;
overflow: hidden;
min-height: 40px;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
font-weight: 600;
font-size: 14px;
line-height: 20px;
}
.goodscard-meta {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 6px;
overflow: hidden;
}
.goodscard-cate {
flex: none;
max-width: 70%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 2px 8px;
background-color: #f5f5f5;
border-radius: 10px;
color: #666;
font-size: 12px;
line-height: 16px;
}
.goodscard-sales {
flex: none;
color: #999;
font-size: 12px;
}
.goodscard-price-row {
display: flex;
align-items: baseline;
margin-top: 8px;
}
.goodscard-price {
color: #ff5722;
font-size: 18px;
font-weight: 600;
}
.goodscard-price em {
font-style: normal;
font-size: 14px;
}
.goodscard-market-price {
margin-left: 8px;
color: #999;
font-size: 12px;
text-decoration: line-through;
}
.goodscard-stock-row {
display: flex;
align-items: center;
margin-top: 8px;
}
.goodscard-stock {
padding: 2px 8px;
border-radius: 10px;
font-size: 12px;
line-height: 16px;
}
.goodscard-stock-out {
color: #ff5722;
background-color: rgba(255, 87, 34, 0.08);
}
.goodscard-stock-in {
color: rgb(22, 183, 119);
background-color: rgba(22, 183, 119, 0.08);
}
.goodscard-footer {
margin-top: 0;
padding: 8px 12px;
}
.goodscard-time .layui-icon {
font-size: 12px;
}
@media (max-width: 768px) {
.goodscard-list {
grid-template-columns: 1fr;
}
}
</style>