feat(view): 名片模板视觉升级,状态开关移至头部

This commit is contained in:
augushong
2026-09-25 11:50:49 +00:00
parent ee13e1bdfe
commit 276d9b001e

View File

@@ -5,17 +5,20 @@
</div>
</div>
<!-- 用户管理名片形态(layoutList 自定义 Vue 形态官方示例,任务 19)
<!-- 用户管理名片形态(layoutList 自定义 Vue 形态官方示例)
硬约束:本节点必须位于页面根容器之外、Vue 渲染容器(#ua-layout-{tableId},
由框架在表格容器后动态创建)之外,否则会被 Vue 编译吞掉。
上下文契约(admin-table.js buildLayoutContext / 任务 17 定稿):
上下文契约(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 输出;纯文本字段用
{{ }} 原生插值(自动转义,与 laytpl 的 {{- }} 不同)
- 事件无需自行绑定:操作按钮与 switch 经页面级委托和 form 事件天然生效 -->
- 事件无需自行绑定:操作按钮与 switch 经页面级委托和 form 事件天然生效
布局 v2(视觉升级):头部左头像+姓名/账户两行、右状态开关(不再独占
一行);主体紧凑小字段(手机/登录次数)+ 备注单行省略;底部上边框分隔,
左创建时间右操作按钮组 -->
<script type="text/x-template" id="usercardTpl">
<div class="usercard-list">
<div class="ua-data-card usercard"
@@ -24,97 +27,204 @@
:data-id="d.id"
:data-table-id="list.tableId"
:class="{'ua-data-card-checked': d._checked, 'ua-data-card-disabled': d._disabled}">
<!-- 头部:圆形头像 + 账户名。head_img 直接取原始行字段绑定 src(渲染器
_cells.head_img 的内联缩略图样式不适合名片);无头像时显示内置图标
占位(当前库内数据均有头像,此分支为约定兜底) -->
<!-- 头部:左 48px 圆形头像(head_img 原始字段直绑,绕开渲染器内联
缩略样式;默认头像占位垫底,图片加载失败 @error 隐藏 img 纯
DOM 降级,不触碰冻结上下文)+ 姓名/登录账户上下两行;
右状态开关悬浮同行:_cells.status 为 switch 渲染器输出的原生
checkbox,容器 layui-form 祖先 + 挂载后 form.render 美化,
切换经页面级 switch 委托提交 modify 接口,失败自动回滚重渲 -->
<div class="usercard-header">
<img v-if="d.head_img" :src="d.head_img" :alt="d.username" class="usercard-avatar">
<span v-else class="usercard-avatar usercard-avatar-empty" title="未设置头像"><i class="layui-icon layui-icon-username"></i></span>
<div class="usercard-name">
<span class="usercard-username">{{ d.username }}</span>
<span v-if="d.nickname" class="usercard-nickname">{{ d.nickname }}</span>
<span class="usercard-avatar-box">
<span class="usercard-avatar usercard-avatar-fallback" title="未设置头像"><i class="layui-icon layui-icon-username"></i></span>
<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'">
</span>
<div class="usercard-id">
<span class="usercard-name" :title="d.nickname || d.username">{{ d.nickname || d.username }}</span>
<span class="usercard-account" :title="d.username">{{ d.username }}</span>
</div>
<div class="usercard-status" v-html="d._cells.status"></div>
</div>
<!-- 状态 switch:_cells.status 为 switch 渲染器输出的原生 checkbox,
容器 layui-form 祖先 + 挂载后 form.render 美化;切换经页面级
switch 委托提交 modify 接口,失败自动回滚重渲 -->
<div class="usercard-status" v-html="d._cells.status"></div>
<!-- 创建时间与手机:无渲染器的纯文本列不进 _cells(值为空串),
直接取原始行字段插值输出(模型获取器已将 create_time 格式化) -->
<div class="usercard-meta">
<span class="usercard-time"><i class="layui-icon layui-icon-time"></i> {{ d.create_time }}</span>
<span v-if="d.phone" class="usercard-phone"><i class="layui-icon layui-icon-cellphone"></i> {{ d.phone }}</span>
<!-- 主体:紧凑小字段两列排布(手机无值显示 —,登录次数为 pill 徽标),
备注有值才渲染、单行省略;均为纯文本原始字段插值 -->
<div class="usercard-body">
<div class="usercard-fields">
<div class="usercard-field">
<span class="usercard-field-label">手机</span>
<span class="usercard-field-value" :title="d.phone">{{ d.phone || '—' }}</span>
</div>
<div class="usercard-field">
<span class="usercard-field-label">登录次数</span>
<span class="usercard-badge">{{ d.login_num }}</span>
</div>
</div>
<div v-if="d.remark" class="usercard-remark" :title="d.remark">{{ d.remark }}</div>
</div>
<!-- 底部:上边框分隔,左创建时间(模型获取器已格式化)右操作按钮组
(_operatHtml 为操作列渲染产物:编辑/设置密码/删除,已按
data-auth-* 裁剪权限),点击行为由框架页面级委托承接 -->
<div class="usercard-footer">
<span class="usercard-time">{{ d.create_time }}</span>
<div class="ua-data-card-operat usercard-operat" v-html="d._operatHtml"></div>
</div>
<!-- 操作按钮组:_operatHtml 为操作列渲染产物(编辑/设置密码/删除,
已按 data-auth-* 裁剪权限),点击行为由框架页面级委托承接 -->
<div class="usercard-operat" v-html="d._operatHtml"></div>
</div>
</div>
</script>
<!-- 名片形态页面级样式(示例边界:网格布局与头像圆形为本页自建,卡片基础
观感——边框/白底/hover/选中描边/操作按钮排布——复用 public.css 的
.ua-data-card 族,不修改公共样式) -->
<!-- 名片形态页面级样式 v2:统一设计语言覆写仅作用于本页 .usercard
(圆角 8px/1px #eee 边框/hover 阴影/选中主题蓝描边+浅蓝底/徽标 pill/
12px 内边距与 8-10px 区块节奏),公共 .ua-data-card 族不动;
网格布局与头像圆形为本页自建 -->
<style>
.usercard-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 12px;
}
.usercard {
padding: 12px;
border: 1px solid #eee;
border-radius: 8px;
}
.usercard:hover {
border-color: #e0e0e0;
box-shadow: 0 2px 12px rgba(0, 0, 0, .08);
}
.usercard.ua-data-card-checked {
border-color: #1e9fff;
box-shadow: 0 0 0 1px #1e9fff inset;
background-color: #f2f9ff;
}
.usercard.ua-data-card-checked:hover {
box-shadow: 0 0 0 1px #1e9fff inset, 0 2px 12px rgba(0, 0, 0, .08);
}
.usercard-header {
display: flex;
align-items: center;
gap: 10px;
}
.usercard-avatar {
.usercard-avatar-box {
position: relative;
flex: none;
width: 48px;
height: 48px;
border-radius: 50%;
object-fit: cover;
border: 1px solid #eee;
background-color: #f6f6f6;
}
.usercard-avatar-empty {
.usercard-avatar {
position: absolute;
top: 0;
left: 0;
width: 48px;
height: 48px;
border-radius: 50%;
border: 1px solid #eee;
background-color: #f6f7f9;
}
.usercard-avatar-fallback {
display: flex;
align-items: center;
justify-content: center;
color: #c2c2c2;
font-size: 24px;
font-size: 22px;
}
.usercard-avatar-img {
object-fit: cover;
}
.usercard-id {
flex: 1;
min-width: 0;
}
.usercard-name {
min-width: 0;
margin-left: 10px;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 20px;
font-size: 14px;
font-weight: 600;
color: #333;
}
.usercard-username {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: bold;
}
.usercard-nickname {
.usercard-account {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 18px;
color: #999;
font-size: 12px;
}
.usercard-status {
margin-top: 8px;
text-align: right;
flex: none;
margin-left: auto;
}
.usercard-meta {
.usercard-status .layui-form-switch {
margin: 0;
}
.usercard-body {
margin-top: 10px;
}
.usercard-fields {
display: flex;
gap: 10px;
}
.usercard-field {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 8px;
gap: 6px;
line-height: 20px;
}
.usercard-field-label {
flex: none;
color: #999;
font-size: 12px;
}
.usercard-meta .layui-icon {
.usercard-field-value {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #333;
font-size: 13px;
}
.usercard-badge {
display: inline-block;
padding: 0 8px;
border-radius: 10px;
background-color: #eef6ff;
color: #1e9fff;
font-size: 12px;
line-height: 18px;
}
.usercard-remark {
margin-top: 6px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #999;
font-size: 12px;
line-height: 18px;
}
.usercard-footer {
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #eee;
}
.usercard-time {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #999;
font-size: 12px;
}
.usercard-operat {
flex: none;
}
@media (max-width: 768px) {
.usercard-list {