Files
ulthon_admin/.agents/skills/ulthon-admin-table/SKILL.md

44 KiB
Raw Blame History

name, description
name description
ulthon-admin-table 后台列表页表格机制与定制完整指南:核心机制(ua 命名空间、三件套)、字段配置、内置渲染器、按钮系统(含页面分类标准 A/B/C/D)、搜索系统(前端配置 + 后端 buildTableParames)、后端配套、常见模式与排错、多形态渲染(受控默认数据流与 controlled:false 逃生口、layoutList 五级模板协议 tplFunction/tplLay/tplVue/tplLayId/tplVueId、双端自动注入、按端 layoutDefault、卡片形态、渲染上下文)。需要业务定制 CURD 生成的列表页(加按钮、改字段、调搜索)、排查按钮/搜索不生效、自定义卡片/模板形态、或判断页面是否需要 controlled:false 逃生口时调用。

后台表格机制与定制(ulthon-admin-table)

本技能是后台列表页(基于 layui table 的 ua.table.render)完整指南。涵盖核心机制、字段配置、按钮系统、搜索机制、后端配套,以及实战中的常见模式与排错。

CURD 生成器生成列表页骨架后,所有业务定制(按钮、字段渲染、搜索)都在本技能范畴内。生成流程本身见 ulthon-scheme-curd-workflow。

何时调用

  • 业务定制 CURD 生成的列表页(加/改字段、加按钮、调搜索)
  • 排查"按钮不显示"、"搜索不生效"、"字段渲染不对"
  • 自定义字段渲染(图片、状态、关联数据)
  • 实现状态流转按钮(带 _if 显隐)
  • 跨控制器跳转按钮
  • 列表自定义卡片/模板形态与表格/卡片切换(layoutList + layoutDefault,见第十一章)
  • 判断页面走的是哪种数据流,或是否需要 controlled: false 逃生口(见第十一章)

一、核心机制总览

1.1 ua 命名空间

ua = ulthon-admin 的 JS 全局对象,定义在 public/static/plugs/ulthon-admin/:

文件 作用
admin-table.js 表格核心:render / formatCols / renderOperat / 内置渲染器 / 搜索监听 / 受控数据流接线(默认数据流与 controlled:false 逃生口分流)与多形态渲染(reloadData shim、双端形态注入、五级模板协议、渲染上下文)
admin-table-store.js 受控数据流数据层(全局 window.UA_TABLE_STORE,所有 ua.table.render 页面的默认数据层):dataProvider / pager / selection / reload 四模块
admin-core.js ua 全局组装、headers、url、parame 等工具
admin-utils.js ua.getDataBrage、ua.checkMobile、ua.getPageSetting 等工具
admin-listen.js 事件监听(ua.listen)
admin-api.js AJAX 请求封装(ua.request)
ulthon-admin.js 入口(把 UA_ADMIN 挂到 window.ua)

1.2 表格三件套

每个 CURD 模块的列表页由三个文件配合:

app/admin/view/<module>/
├── index.html    # 极简 HTML,仅 table 元素 + data-auth-* 权限标记
├── _common.js    # init 对象(URL 配置等)
└── index.js      # ua.table.render({init, cols, toolbar?})

index.html(典型示例,仅 11 行):

<div class="layuimini-container">
    <div class="layuimini-main">
        <table id="currentTable" class="layui-table layui-hide"
               data-auth-index="{:auth('system.timer_config/index')}"
               data-auth-edit="{:auth('system.timer_config/edit')}"
               data-auth-export="{:auth('system.timer_config/export')}"
               data-auth-modify="{:auth('system.timer_config/modify')}"
               lay-filter="currentTable">
        </table>
    </div>
</div>

data-auth-X 是按钮权限显隐的关键:JS 里的 auth: 'X' 必须能在 HTML 找到对应的 data-auth-X,且值为 1 才显示。详见第六章。

1.3 前后端链路

[前端] index.js 的 cols 配置
   │
   ├─ search: 'select'/'range'/false  ──► [前端] renderSearch 自动生成搜索表单
   │                                       │
   │                                       ▼ 用户提交
   │                                  [前端] listenTableSearch 构造 {filter, op} JSON
   │                                       │
   └─ templet: ua.table.xxx            ──► [前端] tool/image/switch 渲染单元格
                                           │
                                           ▼ GET /admin/<module>/index?filter={...}&op={...}
[后端] AdminControllerBase::buildTableParames()
   │
   ├─ 解析 filter + op 为 WHERE 数组(按 op 操作符构造)
   ├─ relationSearch:关联表搜索时自动加 `table.field` 前缀
   └─ 返回 [$page, $limit, $where, $excludes, $request_options, $group]
                                           │
                                           ▼
[后端] 模型查询 → 分页数据 → JSON 返回 → 前端表格渲染

二、init 对象(_common.js 内容)

init 是表格的"配置中心",在 _common.js 中定义,被 index.js 引用。

完整字段表:

字段 类型 说明
tableElem string 表格元素选择器,默认 '#currentTable'
tableRenderId string 表格实例 ID,用于 reload,默认 'currentTableRenderId'
indexUrl string 列表数据 URL(如 'system.timer_config/index')
addUrl string | '' 新增页 URL,空字符串则不显示新增按钮
editUrl string | '' 编辑页 URL
readUrl string | '' 详情页 URL
deleteUrl string | '' 删除接口 URL
exportUrl string | '' 导出接口 URL
modifyUrl string | '' 状态开关修改接口 URL(switch 渲染器自动调用)
formFullScreen 'true' | 'false' 表单是否全屏(字符串)

典型示例(app/admin/view/system/timer_log/_common.js):

var init = {
    tableElem: '#currentTable',
    tableRenderId: 'currentTableRenderId',
    indexUrl: 'system.timer_log/index',
    addUrl: '',
    editUrl: '',
    readUrl: 'system.timer_log/read',
    deleteUrl: '',
    exportUrl: '',
    modifyUrl: '',
};

只读列表(B 类页面,详见第六章)的所有写按钮 URL 留空字符串即可。

三、字段配置(cols 数组项)

ua.table.render({ cols: [[...]] }) 的 cols 是二维数组(layui 标准),每个列项支持以下参数。

3.1 基础参数

参数 类型 说明
field string 数据字段名;支持关联字段如 appCodeBind.tube_code
title string 表头文字
width number 列宽(像素)
minWidth number 最小列宽
sort boolean 是否允许排序
type string 特殊列:'checkbox'(多选)/ 'radio'(单选)
fixed 'left' | 'right' 固定列
edit 'text' 行内编辑(更新到 modifyUrl)
hide boolean 默认隐藏(用户可在列配置里开启)

3.2 渲染参数

参数 类型 说明
templet function 渲染函数,接收 data 返回 HTML。可用内置渲染器(见第五章)或自定义函数
defaultValue string 数据为空时的默认显示值

3.3 搜索参数(详见第七章)

参数 类型 默认值 说明
search boolean | string true 搜索类型:true(文本)/ false(不搜索)/ 'select'(下拉)/ 'number_limit'(数值区间)/ 'time_limit'(时间区间)/ 'range'(范围)
searchOp string '%*%' 搜索操作符,对应后端 WHERE 构造(见 7.1)
selectList object {} 下拉选项;常用 ua.getDataBrage('select_list_xxx') 从页面字典读取
searchUrl string - 远程下拉数据源(如 'system.timer_log/index?selectFields=task_name')
searchValue string - 搜索框默认值
defaultSearchValue string - 默认搜索值(可从 URL query 自动回填)
searchHide boolean false 是否默认隐藏该搜索项
fieldAlias string = field 搜索参数别名(用于关联表搜索时指定 table.field)
searchTip string '请输入' + title 搜索框 placeholder
timeType string 'datetime' 时间搜索组件类型

注意:search 默认是 true(每列默认参与搜索),要关闭必须显式 search: false。

四、内置渲染器(templet 取值)

通过 templet: ua.table.xxx 使用框架内置渲染器。

4.1 内置渲染器清单

渲染器 用途 关键参数
ua.table.image 单图/多图显示 imageWidth(默认 200)/ imageHeight(默认 26)/ imageSplit(默认 |)/ imageJoin(默认 <br>)
ua.table.switch 开关组件(自动调 modify 接口) filter(默认 = field)/ checked(默认 1,等于该值时勾选)
ua.table.list 根据 selectList 映射显示文本 selectList
ua.table.date 日期格式化 (从字段值自动格式化)
ua.table.url 链接显示 urlNameField(链接显示文字字段,可为 string 或 function)
ua.table.filePreview 文件预览(图片或图标) (根据 mime_type 自动判断)
ua.table.tool 行操作按钮渲染器(特殊,见第六章) operat(按钮配置数组)

4.2 使用示例

// 图片字段(自动识别多图,按 | 分隔)
{field: 'head_img', title: '头像', search: false, templet: ua.table.image}

// 状态开关(自动调 modifyUrl,data 字段为 id,value 为 0/1)
{field: 'status', title: '状态', search: 'select',
 selectList: {0: '禁用', 1: '启用'}, templet: ua.table.switch}

// 状态徽章(用 list 把 0/1 映射成文字)
{field: 'status', title: '状态', search: 'select',
 selectList: {0: '禁用', 1: '启用'}, templet: ua.table.list}

// 时间字段
{field: 'create_time', title: '创建时间', search: 'range', templet: ua.table.date}

4.3 自定义 templet 函数

{field: 'is_synced', title: '同步状态', width: 100, templet: function(d) {
    if (d.is_synced == 1) {
        return '<span class="layui-badge layui-bg-green">已同步</span>';
    }
    return '<span class="layui-badge">未同步</span>';
}}

自定义 templet 接收行数据 d,返回 HTML 字符串。

五、按钮系统

后台表格有两类按钮:顶部工具栏(toolbar)和行级操作按钮(operat)。

5.1 顶部工具栏(toolbar)

通过 ua.table.render({ toolbar: [...] }) 配置。不声明时默认 ['refresh', 'add', 'delete', 'export']——这意味着只读页面如果不覆盖,会显示无用的"新增/删除"按钮(见 9.2 常见错误)。

字符串简写(自动检查权限):

值 行为 权限节点
'refresh' 刷新按钮(不带权限) -
'add' 新增按钮,打开 init.addUrl <module>/add
'delete' 批量删除(需配合 checkbox) <module>/delete
'export' 导出按钮,请求 init.exportUrl <module>/export
'selectConfirm' 选择器模式的"确定选择"按钮 -

自定义按钮对象:

ua.table.render({
    toolbar: [
        'add',  // 简写可与自定义混用
        [{
            text: '导入',
            url: 'demo.index/import',
            method: 'open',        // 见 5.3 method 表
            auth: 'import',         // 对应 data-auth-import
            class: 'layui-btn layui-btn-warm layui-btn-sm',
            icon: 'fa fa-upload',
            title: '导入数据',
            extend: 'data-full="true"'  // 额外 HTML 属性
        }]
    ],
    // ...
});

自定义按钮对象参数与行按钮通用,详见 5.4。

5.2 行级操作按钮(operat)

在 cols 最后一列配置 templet: ua.table.tool + operat 数组。不声明 operat 时,tool 函数内自动填充 ['edit', 'delete'](admin-table.js:1055)。

{
    field: '',
    title: '操作',
    width: 250,
    fixed: 'right',
    templet: ua.table.tool,
    operat: [
        'edit',                    // 字符串简写
        [{
            text: '设置密码',
            url: init.password_url,  // 来自 _common.js
            method: 'open',
            auth: 'password',
            class: 'layui-btn layui-btn-normal layui-btn-xs',
            field: 'id',             // URL 拼接的主键字段
            titleField: 'username',  // 弹窗标题用某字段值(如"设置密码 - admin")
        }],
        'delete'
    ]
}

字符串简写:

值 行为 默认 method 权限节点
'edit' 编辑按钮 'tab'(标签页打开) <module>/edit
'delete' 删除按钮(带确认) 'get' <module>/delete

5.3 method 方法表

method 行为 适用场景
'tab' 主内容区导航(layuimini-content-href) 详情页、编辑页(全页面跳转)
'open' 弹窗打开(layer.open iframe) 添加、导入等弹窗表单
'request' AJAX GET 请求 + 自动 confirm 危险/不可逆操作:解绑、撤销、封闭
'blank' 新标签页打开 外部链接、文件预览
'get' 直接 GET 请求(不带 confirm) 内置 'delete' 用此方法
'none' 不绑定行为,配合 extend 自定义监听 自定义 JS 交互

5.4 自定义按钮对象完整参数表

参数 类型 默认值 说明
text string - 按钮文字
title string = text 提示文字 / 弹窗标题 / request 方法的 confirm 文案
url string | function '' 请求 URL;为 function 时接收 (data, operat) 动态构造
method string 'open' 见 5.3 method 表
field string | function 'id' URL 拼接的主键字段名;为 function 时可动态构造 URL
auth string 'add' 权限节点名(对应 HTML 的 data-auth-X)
class string '' HTML class(layui-btn 样式)
icon string '' 图标 class
extend string '' 额外 HTML 属性(如 data-full="true")
titleField string - 用数据中某字段值作为标题(如"编辑 - 张三")
extra string - 标题附加字段(自动拼到 title 前)
_if function | string 始终显示 显隐条件:function 接收 (data, operat) 返回布尔;string 时按字段名取值
callback function - 点击回调(覆盖默认行为)
checkbox boolean false 是否需要选中行
data string[] ['id'] 绑定到 HTML 的数据字段,生成 data-{key}="{value}"

5.5 按钮颜色语义约定

颜色 class 语义
灰色 layui-btn-primary 查看 / 中性操作
绿色 layui-btn-success 编辑 / 正向操作(内置 'edit' 默认)
蓝色 layui-btn-normal 恢复 / 新增入口
橙色 layui-btn-warm 警告 / 可逆危险操作
红色 layui-btn-danger 危险 / 不可逆操作(内置 'delete' 默认)

5.6 _if 条件显隐(最常用的进阶特性)

operat: [
    'edit',
    [{
        text: '触发',
        url: init.triggerUrl,
        method: 'request',
        auth: 'trigger',
        class: 'layui-btn layui-btn-warm layui-btn-xs',
        // 仅 run_type='manual' 时显示
        _if: function(data) {
            return data.run_type === 'manual';
        }
    }]
]

_if 支持两种形式:

  • 函数:function(data) { return boolean },data 是当前行数据
  • 字符串:传字段名,该字段为真才显示(如 '_if': 'is_active')

未声明 _if 时默认恒真(总是显示)。条件按钮缺少 _if 是常见错误(见 9.2)。

5.7 跨控制器权限检查

按钮跳转到其他控制器的页面时(如 box_sn 页面的"绑定"按钮跳转 code_bind/scan),auth 的权限节点不在当前控制器下。需要在 HTML 用自定义 key 声明跨控制器的 data-auth 属性。

HTML:

<table ... data-auth-bind="{:auth('app.code_bind/scan')}" ...>

JS:

[{
    class: 'layui-btn layui-btn-normal layui-btn-sm',
    method: 'tab',
    text: '绑定',
    title: '扫码绑定',
    auth: 'bind',                 // 对应 HTML 中的 data-auth-bind
    url: 'app.code_bind/scan',    // 跨控制器 URL
    icon: 'fa fa-link'
}]

框架通过 data-auth-{auth值} 查找 HTML 属性做权限校验:有权限显示按钮,无权限自动隐藏。

5.8 operat 数组结构

operat 是一个数组,元素有两种形态,可混用:

operat: [
    [/* 按钮组1:每个对象是一个按钮 */],
    [/* 按钮组2 */],
    'edit',   // 字符串简写
    'delete'
]

每个 [] 内的对象是独立按钮,按顺序渲染。renderOperat(admin-table.js:769)做权限整列裁剪:只要任意一个按钮组内有按钮有权限,整列就保留。

六、页面分类标准(A/B/C/D)

根据数据的来源和维护方式,每个列表页必定属于以下某一类。每个页面都应显式声明属于哪一类,并按对应标准配置 toolbar + operat。

A 类:完整 CRUD

数据由后台用户主动创建和维护,支持完整的增删改查。

toolbar: [/* 自定义按钮(如有) */, 'add', 'delete', 'export'],
operat: [[/* 详情 */], [/* 业务操作(如有) */], 'edit', 'delete']

B 类:只读列表

数据由系统流程生成或外部同步,后台仅查看,不支持增删改。

toolbar: [],
defaultToolbar: ['filter'],
operat: [[/* 仅详情 */]]

C 类:有限操作

数据由系统流程生成,不支持通用 add/edit/delete,但支持特定的业务操作(状态流转、撤销等)。

toolbar: [[/* 特定入口按钮 */]/* , 'export'(如需) */],
operat: [[/* 详情 */], [/* 业务按钮(带 _if) */]]
// 不含 'edit' / 'delete',除非有明确的业务需求

D 类:表单交互

非标准列表页,以表单/扫码交互为主,表格仅作为结果展示。

toolbar: [],
// 通常无操作列,或仅详情

七、搜索系统

7.1 前端配置:searchOp 操作符表

cols 项的 searchOp 决定后端如何构造 WHERE:

searchOp 后端 WHERE 构造 适用场景
'%*%'(默认) LIKE '%val%' 通用文本模糊搜索
'*%' LIKE 'val%' 前缀匹配(如编号)
'%*' LIKE '%val' 后缀匹配
'=' = val 精确匹配(如状态)
'in' IN (val) 多值匹配
'min' >= val 数值最小值
'max' <= val 数值最大值
'min_date' >= strtotime(val) 日期最小值(前端传字符串,后端转时间戳)
'max_date' <= strtotime(val) 日期最大值
'range' >= strtotime(begin) AND <= strtotime(end) 时间范围(前端用 search: 'range' 自动生成起止输入)
'none' 不进 where,作为 request_options 业务自定义参数(不查库)

默认操作符是 %*%'(LIKE 模糊匹配)。搜索"不灵活"的错觉来源于不知道可以覆盖 searchOp。

7.2 搜索类型与 searchOp 的关系

search 值 UI 类型 默认提交参数 常配 searchOp
true(默认) 文本输入框 filter[field]=val %*% 或自定义
false 不显示搜索项 - -
'select' 下拉选择 filter[field]=val =
'number_limit' 数值区间(生成 [field]min/[field]max) filter[field]min=val&filter[field]max=val 自动用 min/max
'time_limit' 时间区间 同上但格式为日期 自动用 min_date/max_date
'range' 时间范围选择器 filter[field]=begin - end range

7.3 select_list_* 字典通道(下拉数据来源)

下拉选项的标准通道,三处对齐:

  1. 后端 assign(控制器里):$this->assign('select_list_{名}', $字典数组, true);
    • 第三个参数 true 表示同时压入 data_brage 通道
    • 例:$this->assign('select_list_status', StatusService::SN_STATUS_OPTIONS, true);
  2. 页面渲染:所有 assign 变量被打包成 JSON,渲染进隐藏元素 <script id="data-brage" type="text/plain">{$data_brage|raw|default='[]'}</script>(位于 layout)
  3. 前端读取:ua.getDataBrage('select_list_{名}') 从 #data-brage 解析取值
// cols 中引用:
{field: 'status', search: 'select', selectList: ua.getDataBrage('select_list_status'), ...}

口诀:控制器 assign 的 key 必须等于 cols 里 ua.getDataBrage('xxx') 的参数,否则下拉为空。

7.4 后端处理:buildTableParames

AdminControllerBase::buildTableParames() 自动解析前端传入的 filter + op 参数为 WHERE 数组(extend/base/common/controller/AdminControllerBase.php:405):

// 控制器 index 方法典型用法
public function index()
{
    list($page, $limit, $where, $excludes, $request_options, $group) = $this->buildTableParames();
    $list = $this->model
        ->where($where)
        ->page($page, $limit)
        ->select();
    // ...
}

支持:

  • $excludeFields:buildTableParames(['field_to_exclude']) 排除某些字段不进 where
  • relationSearch:控制器属性 $relationSearch = true 时,自动给字段加 table.field 前缀
  • $request_options:op='none' 的字段会进入此变量(业务自定义参数)

八、后端控制器配套

配套项 说明
index() 使用 buildTableParames() 构造查询
$relationSearch bool 属性,开启关联表搜索(默认 false)
$selectWhere array 属性,selectList() 方法的过滤条件
modify() 状态开关接口,switch 渲染器自动调用,接收 id/field/value
selectList() 下拉数据源接口,接收 selectFields 参数
@NodeAnnotation 控制器方法注解,生成权限节点(与 data-auth-X 对应)

关联搜索示例(extend/base/admin/controller/mall/GoodsBase.php):

class GoodsBase extends AdminController
{
    protected $relationSearch = true;
    // 此时 cols 中 field: 'cate.title' 会自动转为 where: ['mall_goods.cate_id' => ...]
    // 注意:关联字段需在模型里定义好 with 关系
}

九、常见模式与排错

9.1 常见模式速查(参考示例)

场景 参考文件
标准列表(增删改查 + 状态开关) extend/base/admin/view/system/admin/index.js
行内排序(edit: 'text') extend/base/admin/view/system/admin/index.js(sort 字段)
多按钮(设置密码 + 编辑 + 删除) extend/base/admin/view/system/admin/index.js
_if 控制按钮显隐 app/admin/view/system/timer_config/index.js(仅 manual 显示触发按钮)
详情按钮 + 状态徽章 app/admin/view/system/timer_config/index.js
关联表数据展示 extend/base/admin/view/mall/goods/index.js(含 cate 信息)
树形列表 extend/base/admin/view/system/menu/index.js
文件管理(图标预览) extend/base/admin/view/system/uploadfile/index.js
权限分配 UI(双表格) extend/base/admin/view/system/auth/assign_users.js
时间范围搜索 app/admin/view/system/timer_log/index.js(start_time 用 search: 'range')
自定义 templet(多状态徽章) app/admin/view/system/timer_log/index.js(status 字段)

9.2 常见错误模式

错误 1:operat 未声明 → CURD 默认 edit/delete

// 错误:依赖默认值
{ title: '操作', templet: ua.table.tool }
// tool 函数内自动填充 operat = ['edit', 'delete'],即使业务不需要

// 正确:显式声明,哪怕只保留详情
{ title: '操作', templet: ua.table.tool, operat: [[/* 详情 */]] }

错误 2:toolbar 未声明 → 默认含 add/delete/export

// 错误:只读页面(B 类)不覆盖 toolbar
ua.table.render({ init: init, cols: [[...]] });
// 默认 toolbar = ['refresh', 'add', 'delete', 'export'],但 addUrl/deleteUrl 可能为空,按钮仍显示但点击报错

// 正确:只读页面显式清空
ua.table.render({ init: init, toolbar: [], defaultToolbar: ['filter'], cols: [[...]] });

错误 3:条件按钮缺少 _if

// 错误:按钮始终显示,后端校验拒绝
{ text: '解绑', method: 'request', url: init.unbindUrl }

// 正确:前端条件过滤,减少无效点击
{ text: '解绑', method: 'request', url: init.unbindUrl,
  _if: function (data) { return data.status == 可解绑状态值; } }

错误 4:CURD 关联残留列

CURD 生成时会将关联模型的全部字段(如 systemAdmin.*)输出为列。每个页面应清理掉不需要展示的关联残留列,只保留业务需要的字段。

错误 5:select_list_ 不对应(下拉为空)*

控制器 assign('select_list_xxx', ...) 的 key 必须与 cols 里 ua.getDataBrage('select_list_xxx') 的参数完全一致(大小写敏感)。

错误 6:权限节点不匹配(按钮不显示)

  • HTML 的 data-auth-X 必须存在
  • JS 里的 auth: 'X' 必须能找到对应的 data-auth-X
  • {:auth('controller/action')} 的节点路径必须与控制器 @NodeAnnotation 一致

排查工具:php think admin:permission:nodes(见 ulthon-permission-cli)

9.3 排错指南

"按钮不显示"排查步骤:

  1. 检查 HTML 是否声明了对应的 data-auth-X(用浏览器开发者工具看 table 元素)
  2. 检查 {:auth('xxx/yyy')} 的返回值是否为 1(用超管账号测试排除权限问题)
  3. 检查 JS 里 auth: 'X' 的 X 与 HTML data-auth-X 是否完全一致(大小写)
  4. 查看权限节点列表:php think admin:permission:nodes

"搜索不生效"排查步骤:

  1. 浏览器抓 network 看请求是否带 filter 和 op 参数
  2. 检查 filter JSON 是否包含期望的字段
  3. 检查对应字段的 op 值是否符合预期(默认 %*%)
  4. 后端用 php think tools:db:query "SELECT ... WHERE ..." 直接查表确认数据存在
  5. 检查控制器是否调用了 buildTableParames() 而不是手动 where()

十、实测代码引用表

每个特性标注实测来源(多为文件:行;受控/多形态相关特性因行号随迭代漂移,改用 grep 关键词定位),若代码与本文档冲突,以代码为准。

特性 实测来源
ua.table.render 入口 public/static/plugs/ulthon-admin/admin-table.js:14
formatCols(cols 加工) admin-table.js:921
renderOperat(操作列整列裁剪) admin-table.js:769
renderToolbar(顶部工具栏) admin-table.js:251
renderSearch(搜索表单生成) admin-table.js:294
search 默认 true admin-table.js:307(ua.parame(d.search, true))
searchOp 默认 '%*%' admin-table.js:312
search: 'number_limit' / 'time_limit' admin-table.js:348-369
listenTableSearch(filter+op 构造) admin-table.js:1334
tool 函数(自动填充默认 operat) admin-table.js:1053,自动填充在 1055
tool 内置 'edit' 处理 admin-table.js:1061
tool 内置 'delete' 处理 admin-table.js:1082
operat._if 显隐判定 admin-table.js:1113-1124
operat.auth 默认 'add' admin-table.js:1105
list 渲染器 admin-table.js:1169
filePreview 渲染器 admin-table.js:1180
image 渲染器(imageSplit/imageJoin) admin-table.js:1195-1213
url 渲染器(urlNameField) admin-table.js:1215
switch 渲染器(自动调 modify) admin-table.js:1245,modify 调用在 listenSwitch:1365
date 渲染器 admin-table.js:1281
ua.getDataBrage 实现 public/static/plugs/ulthon-admin/admin-utils.js:63-74
#data-brage 隐藏元素 extend/base/admin/view/layout/default.html(layout 文件)
data_brage assign extend/base/common/controller/AdminControllerBase.php:273
buildTableParames(filter+op 解析) extend/base/common/controller/AdminControllerBase.php:405
$relationSearch 属性 AdminControllerBase.php:69(默认 false)
$selectWhere 属性 AdminControllerBase.php:63
selectList() 方法 AdminControllerBase.php:489
实际示例:标准 CRUD extend/base/admin/view/system/admin/index.js
实际示例:_if 条件按钮 app/admin/view/system/timer_config/index.js
实际示例:自定义 templet app/admin/view/system/timer_log/index.js
受控默认数据流 / 逃生口分叉 grep "controlled !== false" public/static/plugs/ulthon-admin/admin-table.js(注入门控、数据流分叉、store 接线共三处)
受控接线(注册 store、自建分页与模板容器、首屏取数) grep "受控接线" admin-table.js
逃生口分支(引擎 parseData 装配保留) grep "逃生口" admin-table.js
reloadData shim(受控分流/透传) grep "__uaOriginalReloadData" admin-table.js
ua.table.reloadData / ua.table.checkStatus 分发入口 grep "受控/非受控分发入口" admin-table.js
store 五入口(register/has/reload/fetch/checkStatus) grep "五个主入口" public/static/plugs/ulthon-admin/admin-table-store.js
layoutList 规范化(字符串/对象条目) grep "normalizeLayoutList" admin-table.js
双端自动注入与视图按钮(未配 layoutList 注入 ['table','card']) grep "autoInjected" admin-table.js
显式 defaultToolbar 时视图按钮补投(去重) grep "去重追加" admin-table.js
按端默认形态解析(记忆 > 按端显式 > 端缺省三层链) grep "defaultLayoutName" admin-table.js
形态记忆读/写(table-layout-default) grep "table-layout-default" admin-table.js
五级钩子优先级解析 grep "resolveLayoutEngine" admin-table.js
内置 card 模板(tplFunction) grep "builtinCardTemplate" admin-table.js
渲染上下文构建器 grep "buildLayoutContext" admin-table.js
tplFunction/laytpl/Vue 调用约定注释 grep "列表级单实例" admin-table.js
renderLayoutForm(模板管线入口) grep "renderLayoutForm: function" admin-table.js
统一行选中委托(data-table-id + data-id) grep "统一行选中委托" admin-table.js
getLayoutForm / setLayoutForm grep "getLayoutForm" admin-table.js
ua.checkMobile 判定 grep "checkMobile" public/static/common/js/app.js(admin-utils.js 转发)
实际示例:三形态 + 按端默认 extend/base/admin/view/system/admin/index.js(render 配置块:layoutList 三形态 + layoutDefault 对象式)+ index.html(usercardTpl 模板节点)

十一、多形态渲染(受控数据流与自定义模板)

ua.table.render 页面默认运行在受控数据流上,并默认获得表格/卡片两种形态,两者都是零配置即得。开发者只需要回答两个问题:想定制形态怎么配(layoutList + layoutDefault,见 11.2),以及自己的页面是否属于少数需要逃生口的场景(controlled: false,见 11.1)。官方完整范例是用户管理页:extend/base/admin/view/system/admin/index.js 顶部 render 配置里的 layoutList 三形态与 layoutDefault 对象式按端默认,建议直接对照源码。

11.1 受控数据流(默认)与逃生口

受控是默认数据流:ua.table.render 不传 controlled 即受控,页面数据由 admin-table-store.js(全局 window.UA_TABLE_STORE)接管,引擎只做纯渲染,不再自己发请求。首屏、搜索、排序、翻页、switch 回滚、删除刷新等所有数据重载统一走 reloadData 的 shim 分流进 store。

组件 说明
shim(table.reloadData 包装) 受控表(store 已注册该 id)→ store.reload;非受控表原样透传引擎
ua.table.reloadData(id, opts) 页面代码刷新表格的推荐入口:按受控与否自动分流,受控/非受控页面通吃
ua.table.checkStatus(id) 取选中行的推荐入口,同样自动分流

旧页面里直呼 layui.table.reloadData / layui.table.checkStatus 的代码仍会被 shim 覆盖到,但新代码请统一用 ua.table.reloadData / ua.table.checkStatus。

store 内部四个模块(admin-table-store.js):

模块 职责
dataProvider 取数(fetch)与状态:where / sort / 当页数据 / count
pager laypage 封装;受控表分页条由框架自建(.ua-store-pager),页面不用管
selection 选中集合:跨页保持勾选,与引擎勾选是同一事实源
reload 重载编排:合并 where/page 后 fetch,成功回调里按当前形态分发渲染

对外五个主入口 register / has / reload / fetch / checkStatus。业务页面一般不直接调 store,用上表两个 ua.table.* 分发入口就够了。

受控页的模板渲染容器:受控表在引擎视图与分页条之间有一个模板形态容器,元素 id 为 ua-layout-{tableRenderId}(默认页面即 #ua-layout-currentTableRenderId,取的是 render 实例 id,不是表格元素 id,写选择器时勿混淆)。模板形态的内容渲染在这里,与引擎数据区互斥显隐;工具栏与搜索区两形态共用(卡片形态下新增/批量删除/搜索照常可用)。

逃生口 controlled: false:受控数据流覆盖绝大多数页面,少数下游页面依赖引擎 url 模式的行为,典型场景是自定义了 parseData 改写响应结构、或直接消费引擎请求管线。这类页面显式传 controlled: false 退回引擎 url 模式:引擎自己取数与分页,页面自定义 parseData 与选择态注入照常生效。注意逃回的代价:不注入形态清单、工具栏没有视图切换按钮、恒为表格形态,多形态渲染整体不可用;ua.table.reloadData 等分发入口对其透传引擎。

// 逃生口示例:仅依赖引擎 url 模式行为的页面才加这一行
ua.table.render({
    init: init,
    controlled: false,  // 退回引擎 url 模式,放弃多形态
    cols: [[/* ... */]],
});

11.2 形态清单(layoutList)与双端自动注入

双端自动注入:受控页不配置 layoutList 时,框架双端自动注入 ['table', 'card'],条目数大于 1,双端工具栏自动出现「视图」切换按钮。显式配置了 layoutList 的页面保持显式配置优先,不注入。layoutList: ['table'] 单条目即锁定表格出口:无视图按钮,不可切形态。

layoutList 声明表格支持的形态清单,条目有两种写法:

// 字符串条目:'table'/'card' 有内置实现(标题缺省「表格」/「卡片」),
// 其他字符串以自身为标题
layoutList: ['table', 'card']

// 对象条目:自定义形态,name 必填,title 供视图切换下拉显示,icon 可选
layoutList: ['table', {name: 'usercard', title: '名片', tplVueId: '#usercardTpl'}]

模板钩子五级优先级(按序取第一个命中的,提供一种即生效):

优先级 钩子 类型 引擎
1 tplFunction function 框架行循环,(行上下文, 列表上下文) 返回单行 HTML 字符串
2 tplLay string laytpl 模板字符串,行级作用域
3 tplVue string Vue 模板字符串,列表级单实例
4 tplLayId string <script type="text/html"> 节点选择器(laytpl)
5 tplVueId string <script type="text/x-template"> 节点选择器(Vue)
  • 五种钩子全无且条目不是 table:console.warn(每条目仅一次)并回退表格形态
  • 内置 card 本质是一个框架内置的 tplFunction 条目(header/body/footer 卡片骨架,样式类 .ua-data-card 族);条目自带任意钩子时完全覆盖内置实现
  • 模板渲染失败(节点缺失、laytpl 错误、Vue 未引入或挂载失败)自动 console.error 并回退表格形态,不白屏;回退不写记忆,修复后可再切换

按端默认形态(layoutDefault):推荐对象式写法,按端声明默认形态。

layoutDefault: {pc: 'table', mobile: 'card'},  // 官方示例页写法:按端取值
layoutDefault: 'card',                          // 旧写法兼容:字符串,双端同值
layoutDefault: true,                            // 旧协议"自动判断",视为未配置走端缺省

默认形态按三层链解析(优先级从高到低),端判定用 ua.checkMobile():

  1. 记忆:localStorage 按页记忆,键 table-layout-default(经 ua.getPageSetting/ua.setPageSetting 带页面键前缀存储);值不在形态清单内忽略
  2. 按端显式:layoutDefault 按当前端取值,对象式 pc/mobile 键缺省视为该端未配置;取值不在清单内忽略
  3. 端缺省:PC 取 table,无则清单首条目;mobile 取 card,无则 table,再无则首条目

形态清单为空时恒为 table:非受控逃生口页,或 layoutList 配置了但所有条目都无效(垃圾清单),都不会在空清单上取首条目。

视图切换与 CURD 缺省:

  • layoutList 条目数 > 1 时工具栏出现「视图」按钮;点击下拉条目即写记忆并切形态,切换用的是 store 当页数据,零网络请求
  • 页面显式配置了 defaultToolbar 时,视图按钮会自动补投进最终工具栏(自动注入场景下去重追加),普通开发者无需关心
  • 运行期 API:ua.table.getLayoutForm(id) 读当前形态、ua.table.setLayoutForm(id, name) 切形态
  • CURD 页面缺省即得双形态:CURD 生成的列表页不含任何受控/形态配置,自动获得受控数据流 + 双端「表格/卡片」+ 视图按钮,模板零改动;PC 端缺省表格、手机端缺省卡片

两条硬约束:

  1. 模板渲染容器恒带 layui-form 类与 lay-filter(框架自建容器已保证,页面无需关心)。表单控件的渲染美化与事件都要求 .layui-form 祖先,模板里写 switch/checkbox 依赖这条保证,缺失则全部失效。
  2. laytpl/Vue 的模板节点(<script type="text/html"> / text/x-template)必须写在渲染容器之外(页面根容器之后即可)。节点若写进容器内会被模板引擎编译时吞掉。这是使用者必须遵守的约束:框架不生成也不搬运模板节点。

11.3 渲染上下文

自定义模板拿到的数据由 ua.table.buildLayoutContext(tableId) 构建(调试时可直接在控制台调用查看)。

行上下文(tplFunction 第一参 / laytpl 的 d / Vue 的 v-for 项):

字段 说明
...row 行原始字段全部可用(username、status、create_time 等)
_cells 对象,field → 该列 templet 渲染后的 HTML。只有配了渲染器的列才有内容;纯文本列恒为空串,勿引用,直接用原始字段
_operatHtml 操作列渲染产物(编辑/删除等按钮 HTML,已按 data-auth-* 裁剪权限)
_checked 该行是否选中(store 选中集合)
_disabled 该行是否禁选(行数据 LAY_DISABLED)

列表上下文(tplFunction 第二参):{tableId, rows, page, count, checkedAll}。laytpl 形态挂在 d._list 上;Vue 形态叫 list,因为 Vue 实例不代理 _/$ 前缀的数据键,裸写 _list 会 ReferenceError。

11.4 自定义形态示例

laytpl 形态(tplLayId),简版:

<!-- 页面模板区:渲染容器之外 -->
<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'}],

laytpl 语法实测(layui 2.10.1 内置 laytpl v2.0.0):{{ d.x }} 与 {{= d.x }} 都会 HTML 转义,{{- d.x }} 才是原样输出。_cells/_operatHtml 是渲染产物 HTML,必须用 {{- }};纯文本用 {{ }} 转义更安全;{{# }} 写 JS 语句;列表上下文是 d._list。

Vue 形态(tplVueId),简版:

<!-- 页面模板区:渲染容器之外 -->
<script type="text/x-template" id="myVueTpl">
    <div class="my-card" v-for="d in rows" :key="d.id"
         :data-id="d.id" :data-table-id="list.tableId"
         :class="{'ua-data-card-checked': d._checked}">
        <h3 v-html="d._cells.title"></h3>
        <div v-html="d._operatHtml"></div>
    </div>
</script>
layoutList: ['table', {name: 'mycard', title: '自定义卡', tplVueId: '#myVueTpl'}],
  • v-for="d in rows" 承接行循环;_cells/_operatHtml 必须 v-html 输出,纯文本字段用 {{ }} 原生插值(自动转义);列表上下文是 list
  • 行根元素带 data-table-id + data-id 两属性即获得统一行选中委托:点卡片切换勾选、选中态经 :class 绑 ua-data-card-checked 高亮回显;按钮/表单控件自动豁免不误触,零 JS
  • 事件无需自己绑定:操作按钮(data-open/data-request 等属性)与 switch/checkbox 经页面级委托与 form 事件天然生效;@click 可用,但处理函数内不要修改模板数据(渲染上下文被冻结,数据更新一律整卡销毁重建)

完整官方范例:用户管理三形态(表格 + 名片 + 内置卡片),建议直接对照源码:

位置 内容
extend/base/admin/view/system/admin/index.js render 配置块:layoutList 三形态 + layoutDefault 对象式按端默认({pc: 'table', mobile: 'card'})
extend/base/admin/view/system/admin/index.html usercardTpl 模板节点(<script type="text/x-template">,位于渲染容器之外)+ 页面级网格样式

该范例演示了三种典型取值通道:渲染器列(status)用 _cells + v-html 承接框架语义、纯文本列(username/create_time/phone)直接原始字段插值、头像(head_img)因名片需要圆形大图绕开渲染器直接绑原始字段。模板形态的网格布局由模板自带容器承担(Vue 形态不带内置卡片网格类)。

11.5 双端默认行为

  • 双端自动注入后 PC 端同样有视图切换按钮与卡片形态,端差异只体现在缺省形态上:PC 缺省 table,手机缺省 card(记忆与按端 layoutDefault 的优先级更高,见 11.2 解析链)
  • 手机端判定 ua.checkMobile()(admin-utils.js 转发,实现位于 public/static/common/js/app.js):UA 关键字(Android/iPhone/iPad 等)或 window.screen 宽 < 600 且高 < 800。判的是物理屏幕指标,浏览器开发者工具仅改视口宽度不会触发;设备仿真验证手机默认形态时,必须同时覆写 UA 或仿真 screen 尺寸,仅设 viewport 无效
  • 卡片形态能力边界:工具栏(刷新/新增/批量删除/导出)与搜索区两形态共用、照常可用;勾选语义由卡片勾位图标与选中委托承接(checkbox 列本身不进模板上下文);_disabled 行整元素不可勾选

相关技能