mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-11-10 13:00:49 +08:00
792 lines
44 KiB
Markdown
792 lines
44 KiB
Markdown
---
|
||
name: "ulthon-admin-table"
|
||
description: "后台列表页表格机制与定制完整指南:核心机制(ua 命名空间、三件套)、字段配置、内置渲染器、按钮系统(含页面分类标准 A/B/C/D)、搜索系统(前端配置 + 后端 buildTableParames)、后端配套、常见模式与排错、多形态渲染机制(受控默认数据流与 controlled:false 逃生口、layoutList 五级模板协议 tplFunction/tplLay/tplVue/tplLayId/tplVueId、双端自动注入、layoutDefault 按端默认与记忆链、卡片形态)。需要业务定制 CURD 生成的列表页(加按钮、改字段、调搜索)、排查按钮/搜索不生效、配置 layoutList/layoutDefault/视图切换,或判断页面是否需要 controlled:false 逃生口时调用;自定义卡片模板的创作(上下文契约、实战配方、设计模式、坑与验收)见 ulthon-card-template 技能。"
|
||
---
|
||
|
||
# 后台表格机制与定制(ulthon-admin-table)
|
||
|
||
本技能是后台列表页(基于 layui table 的 `ua.table.render`)完整指南。涵盖核心机制、字段配置、按钮系统、搜索机制、后端配套,以及实战中的常见模式与排错。
|
||
|
||
CURD 生成器生成列表页骨架后,所有业务定制(按钮、字段渲染、搜索)都在本技能范畴内。生成流程本身见 [ulthon-scheme-curd-workflow](./ulthon-scheme-curd-workflow/SKILL.md)。
|
||
|
||
## 何时调用
|
||
|
||
- 业务定制 CURD 生成的列表页(加/改字段、加按钮、调搜索)
|
||
- 排查"按钮不显示"、"搜索不生效"、"字段渲染不对"
|
||
- 自定义字段渲染(图片、状态、关联数据)
|
||
- 实现状态流转按钮(带 `_if` 显隐)
|
||
- 跨控制器跳转按钮
|
||
- 列表多形态机制与表格/卡片切换(`layoutList` + `layoutDefault`,见第十一章;自定义卡片模板的创作见 [ulthon-card-template](./ulthon-card-template/SKILL.md) 技能)
|
||
- 判断页面走的是哪种数据流,或是否需要 `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 # 纯结构零权限声明,权限由 layout 全局 UA_AUTH 注入
|
||
├── _common.js # init 对象(URL 配置等)
|
||
└── index.js # ua.table.render({init, cols, toolbar?})
|
||
```
|
||
|
||
**index.html**(典型示例):
|
||
|
||
```html
|
||
<div class="layuimini-container">
|
||
<div class="layuimini-main">
|
||
<table id="currentTable" class="layui-table layui-hide"
|
||
lay-filter="currentTable">
|
||
</table>
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
**权限判定协议**:index.html 不写任何权限声明。页面加载时 layout 注入全局载荷 `window.UA_AUTH = {all, controller, nodes}`(服务端一次性构建,超管 `all:true`),前端 `ua.auth(node)` 据此全局判定按钮显隐:短名(不含 `/`)自动补当前控制器前缀,跨控制器直接写全节点(如 `'app.code_bind/scan'`,与注册节点逐字一致,动作段大小写敏感)。详见 5.4 与 5.7。
|
||
|
||
> **升级注意**:本机制的判定实现位于 admin-core.js,其静态引用带框架版本号缓存键(下游部署为 site_version)。升级框架后若按钮显隐异常,先强制刷新浏览器缓存(Ctrl+F5)再排查,避免旧缓存 JS 与新页面叠加造成误判。
|
||
|
||
### 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`):
|
||
|
||
```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 使用示例
|
||
|
||
```js
|
||
// 图片字段(自动识别多图,按 | 分隔)
|
||
{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 函数
|
||
|
||
```js
|
||
{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'` | 选择器模式的"确定选择"按钮 | - |
|
||
|
||
**自定义按钮对象**:
|
||
|
||
```js
|
||
ua.table.render({
|
||
toolbar: [
|
||
'add', // 简写可与自定义混用
|
||
[{
|
||
text: '导入',
|
||
url: 'demo.index/import',
|
||
method: 'open', // 见 5.3 method 表
|
||
auth: 'import', // 短名=当前控制器动作(等价 'demo.index/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']`**。
|
||
|
||
```js
|
||
{
|
||
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'` | 权限节点:短名=当前控制器动作,跨控制器写全节点(与注册节点逐字一致,详见 5.7) |
|
||
| `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` 条件显隐(最常用的进阶特性)
|
||
|
||
```js
|
||
operat: [
|
||
'edit',
|
||
[{
|
||
text: '触发',
|
||
url: init.triggerUrl,
|
||
method: 'request',
|
||
auth: 'trigger', // 短名=当前控制器动作(等价 'system.timer_config/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 中转声明:
|
||
|
||
```js
|
||
[{
|
||
class: 'layui-btn layui-btn-normal layui-btn-sm',
|
||
method: 'tab',
|
||
text: '绑定',
|
||
title: '扫码绑定',
|
||
auth: 'app.code_bind/scan', // 全节点,与注册节点逐字一致
|
||
url: 'app.code_bind/scan', // 跨控制器 URL
|
||
icon: 'fa fa-link'
|
||
}]
|
||
```
|
||
|
||
`ua.auth` 按全局允许集(layout 注入的 `window.UA_AUTH`)判定:有权限显示按钮,无权限自动隐藏。
|
||
|
||
**全节点写法约定**:
|
||
|
||
- 与 `@NodeAnnotation` 注册的节点**逐字一致**:`ua.auth` 做字符串匹配,前端不做任何规范化转换
|
||
- 动作段**大小写敏感**:注册为 `setMaster` 就写 `'system.host/setMaster'`,禁止 snake 化写成 `set-master`(parseNodeStr 只规范控制器段,动作段原样保留)
|
||
- 短名(不含 `/`)自动按"当前控制器/短名"解析,只适用于本控制器动作
|
||
|
||
**未注册动作的行为差异**:权限载荷不含 default_pass,前端对"不在允许集"的按钮一律保守隐藏。在 `default_auth_check=true` 的部署中,未注册动作后端仍按真实规则放行,即前端隐藏而后端放行,此时应前后端核对并补注册节点。旧协议(HTML `data-auth` 属性中转)仅作为下游旧页面的回退通道保留,新页面不使用。
|
||
|
||
### 5.8 operat 数组结构
|
||
|
||
`operat` 是一个数组,元素有两种形态,可混用:
|
||
|
||
```js
|
||
operat: [
|
||
[/* 按钮组1:每个对象是一个按钮 */],
|
||
[/* 按钮组2 */],
|
||
'edit', // 字符串简写
|
||
'delete'
|
||
]
|
||
```
|
||
|
||
每个 `[]` 内的对象是独立按钮,按顺序渲染。`renderOperat`(定位见第十章速查表)做权限整列裁剪:只要任意一个按钮组内有按钮有权限,整列就保留。
|
||
|
||
## 六、页面分类标准(A/B/C/D)
|
||
|
||
根据数据的来源和维护方式,每个列表页必定属于以下某一类。**每个页面都应显式声明属于哪一类,并按对应标准配置 toolbar + operat。**
|
||
|
||
### A 类:完整 CRUD
|
||
|
||
数据由后台用户主动创建和维护,支持完整的增删改查。
|
||
|
||
```js
|
||
toolbar: [/* 自定义按钮(如有) */, 'add', 'delete', 'export'],
|
||
operat: [[/* 详情 */], [/* 业务操作(如有) */], 'edit', 'delete']
|
||
```
|
||
|
||
### B 类:只读列表
|
||
|
||
数据由系统流程生成或外部同步,后台仅查看,不支持增删改。
|
||
|
||
```js
|
||
toolbar: [],
|
||
defaultToolbar: ['filter'],
|
||
operat: [[/* 仅详情 */]]
|
||
```
|
||
|
||
### C 类:有限操作
|
||
|
||
数据由系统流程生成,不支持通用 add/edit/delete,但支持特定的业务操作(状态流转、撤销等)。
|
||
|
||
```js
|
||
toolbar: [[/* 特定入口按钮 */]/* , 'export'(如需) */],
|
||
operat: [[/* 详情 */], [/* 业务按钮(带 _if) */]]
|
||
// 不含 'edit' / 'delete',除非有明确的业务需求
|
||
```
|
||
|
||
### D 类:表单交互
|
||
|
||
非标准列表页,以表单/扫码交互为主,表格仅作为结果展示。
|
||
|
||
```js
|
||
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` 解析取值
|
||
|
||
```js
|
||
// 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`):
|
||
|
||
```php
|
||
// 控制器 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` | 控制器方法注解,生成权限节点(前端 `ua.auth` 全局判定按钮显隐的依据) |
|
||
|
||
**关联搜索示例**(`extend/base/admin/controller/mall/GoodsBase.php`):
|
||
|
||
```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**
|
||
|
||
```js
|
||
// 错误:依赖默认值
|
||
{ title: '操作', templet: ua.table.tool }
|
||
// tool 函数内自动填充 operat = ['edit', 'delete'],即使业务不需要
|
||
|
||
// 正确:显式声明,哪怕只保留详情
|
||
{ title: '操作', templet: ua.table.tool, operat: [[/* 详情 */]] }
|
||
```
|
||
|
||
**错误 2:toolbar 未声明 → 默认含 add/delete/export**
|
||
|
||
```js
|
||
// 错误:只读页面(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**
|
||
|
||
```js
|
||
// 错误:按钮始终显示,后端校验拒绝
|
||
{ 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:权限节点不匹配(按钮不显示)**
|
||
|
||
- JS 里的 auth 值解析出的节点必须与控制器 `@NodeAnnotation` 注册的节点一致:短名等于"当前控制器/动作",跨控制器直接写全节点
|
||
- 全节点与注册节点逐字比对,动作段大小写敏感(注册 `setMaster` 写成 `set-master` 即失配隐藏)
|
||
- 未注册动作前端保守隐藏;`default_auth_check=true` 部署下后端仍按真实规则放行,前后端核对后补注册节点
|
||
|
||
排查工具:`php think admin:permission:nodes`(见 [ulthon-permission-cli](./ulthon-permission-cli/SKILL.md))
|
||
|
||
### 9.3 排错指南
|
||
|
||
**"按钮不显示"排查步骤**:
|
||
|
||
1. 浏览器控制台核对 `window.UA_AUTH`:确认按钮 auth 值解析出的节点在允许集内(短名自动补当前控制器前缀,全节点逐字匹配)
|
||
2. 检查 JS 里 auth 值与 `@NodeAnnotation` 注册节点是否逐字一致(动作段大小写敏感,禁止 snake 化动作段)
|
||
3. 用超管账号测试排除账号权限配置问题(超管 `all:true` 恒显)
|
||
4. 查看权限节点列表:`php think admin:permission:nodes`
|
||
5. 下游旧页面(data-auth 回退通道):检查 HTML 是否声明了对应的 `data-auth-X` 属性且值为 1。仅旧页面有此声明,新协议页面 index.html 无权限声明
|
||
|
||
**"搜索不生效"排查步骤**:
|
||
|
||
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`(`grep "render: function (options)"`) |
|
||
| `formatCols`(cols 加工) | `admin-table.js`(`grep "formatCols: function"`) |
|
||
| `renderOperat`(操作列整列裁剪) | `admin-table.js`(`grep "renderOperat(data, elem)"`) |
|
||
| `renderToolbar`(顶部工具栏) | `admin-table.js`(`grep "renderToolbar: function"`) |
|
||
| `renderSearch`(搜索表单生成) | `admin-table.js`(`grep "renderSearch: function"`) |
|
||
| `search` 默认 `true` | `admin-table.js`(`grep "d.search, true"`) |
|
||
| `searchOp` 默认 `'%*%'` | `admin-table.js`(`grep "d.searchOp"`) |
|
||
| `search: 'number_limit'` / `'time_limit'` | `admin-table.js`(`grep "number_limit"`) |
|
||
| `listenTableSearch`(filter+op 构造) | `admin-table.js`(`grep "listenTableSearch: function"`) |
|
||
| `tool` 函数(自动填充默认 operat) | `admin-table.js`(`grep "operat = option.operat"`,tool 函数内) |
|
||
| `tool` 内置 'edit' / 'delete' 处理 | 同上 tool 函数内 `case 'edit'` / `case 'delete'` 分支 |
|
||
| `operat._if` 显隐判定 | `admin-table.js`(`grep "operat._if"`) |
|
||
| `operat.auth` 默认 `'add'` | `admin-table.js`(`grep "operat.auth = operat.auth \|\|"`) |
|
||
| `list` 渲染器 | `admin-table.js`(`grep "list: function"`) |
|
||
| `filePreview` 渲染器 | `admin-table.js`(`grep "filePreview: function"`) |
|
||
| `image` 渲染器(imageSplit/imageJoin) | `admin-table.js`(`grep "image: function"`) |
|
||
| `url` 渲染器(urlNameField) | `admin-table.js`(`grep "url: function"`) |
|
||
| `switch` 渲染器(自动调 modify) | `admin-table.js`(`grep "switch: function"`),modify 调用在 `grep "listenSwitch: function"` |
|
||
| `date` 渲染器 | `admin-table.js`(`grep "date: function"`) |
|
||
| `ua.getDataBrage` 实现 | `admin-utils.js`(`grep "ua.getDataBrage = function"`) |
|
||
| `#data-brage` 隐藏元素 | `extend/base/admin/view/layout/default.html`(layout 文件) |
|
||
| `data_brage` assign | `AdminControllerBase.php`(`grep "assign('data_brage'"`) |
|
||
| `buildTableParames`(filter+op 解析) | `AdminControllerBase.php`(`grep "function buildTableParames"`) |
|
||
| `$relationSearch` 属性 | `AdminControllerBase.php`(`grep "protected $relationSearch"`,默认 false) |
|
||
| `$selectWhere` 属性 | `AdminControllerBase.php`(`grep "protected $selectWhere"`) |
|
||
| `selectList()` 方法 | `AdminControllerBase.php`(`grep "public function selectList"`) |
|
||
| 实际示例:标准 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-data-card` 族) | `grep "ua-data-card" public/static/admin/css/public.css` |
|
||
| `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 模板节点);第二官方范例 `extend/base/admin/view/mall/goods/`(goodscardTpl + `layoutDefault: 'goodscard'` 字符串写法) |
|
||
|
||
## 十一、多形态渲染机制(受控数据流与形态协议)
|
||
|
||
`ua.table.render` 页面默认运行在受控数据流上,并默认获得表格/卡片两种形态,两者都是零配置即得。本章只讲机制:数据流与逃生口(11.1)、形态清单协议与解析链(11.2)、双端默认行为(11.3)。要为自己的页面定制专属卡片模板(适用判断、上下文契约、实战配方、设计模式库、坑与验收),见 [ulthon-card-template](./ulthon-card-template/SKILL.md) 技能。
|
||
|
||
### 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` 等分发入口对其透传引擎。
|
||
|
||
```js
|
||
// 逃生口示例:仅依赖引擎 url 模式行为的页面才加这一行
|
||
ua.table.render({
|
||
init: init,
|
||
controlled: false, // 退回引擎 url 模式,放弃多形态
|
||
cols: [[/* ... */]],
|
||
});
|
||
```
|
||
|
||
### 11.2 形态清单(layoutList)与双端自动注入
|
||
|
||
**双端自动注入**:受控页不配置 `layoutList` 时,框架双端自动注入 `['table', 'card']`,条目数大于 1,双端工具栏自动出现「视图」切换按钮。显式配置了 `layoutList` 的页面保持显式配置优先,不注入。`layoutList: ['table']` 单条目即锁定表格出口:无视图按钮,不可切形态。
|
||
|
||
`layoutList` 声明表格支持的形态清单,条目有两种写法:
|
||
|
||
```js
|
||
// 字符串条目:'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)**:推荐对象式写法,按端声明默认形态。
|
||
|
||
```js
|
||
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)` 切形态。API 切换**不写记忆**,只有点视图下拉条目才写 `ua.setPageSetting`;API 切换后刷新会回到记忆形态,这是设计行为
|
||
- **CURD 页面缺省即得双形态**:CURD 生成的列表页不含任何受控/形态配置,自动获得受控数据流 + 双端「表格/卡片」+ 视图按钮,模板零改动;PC 端缺省表格、手机端缺省卡片
|
||
|
||
**两条硬约束**:
|
||
|
||
1. 模板渲染容器恒带 `layui-form` 类与 lay-filter(框架自建容器已保证,页面无需关心)。表单控件的渲染美化与事件都要求 `.layui-form` 祖先,模板里写 switch/checkbox 依赖这条保证,缺失则全部失效。
|
||
2. 模板作者侧另有一条位置硬约束(模板节点必须写在渲染容器之外,否则被模板引擎编译时吞掉),约束细节、页面写法与模板契约由 [ulthon-card-template](./ulthon-card-template/SKILL.md) 技能第三章承接。
|
||
|
||
### 11.3 双端默认行为
|
||
|
||
- 双端自动注入后 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 无效
|
||
- 卡片形态能力边界:工具栏(刷新/新增/批量删除/导出)与搜索区两形态共用、照常可用;行选中、豁免与 `_disabled` 等模板侧语义由卡片模板契约定义(见 11.4);`_cells` 隐藏列(col.hide)不产出对应键,模板按原始字段直取即可
|
||
|
||
### 11.4 自定义模板指引
|
||
|
||
自定义卡片/模板形态的创作不在本技能展开,统一由 [ulthon-card-template](./ulthon-card-template/SKILL.md) 技能承接:渲染上下文契约(`_cells`/`_operatHtml`/`_checked`/`_disabled`、上下文冻结、列表上下文命名)、模板节点位置硬约束、行选中委托与豁免名单(勾位契约:点行内 `.ua-data-card-check` 勾位元素切换勾选,勾位元素须在行内,行根元素仍需携带 `data-table-id` + `data-id` 两属性)、取值通道三规则、usercardTpl/goodscardTpl 范例走读、实战配方、设计模式库、模板作者坑与验收 checklist。本技能只负责机制如何把模板跑起来;两个官方范例的 render 配置块(layoutList 三形态、layoutDefault 对象式与字符串式)见第十章引用表末行,可作机制示范直接对照。
|
||
|
||
## 相关技能
|
||
|
||
- [ulthon-card-template](./ulthon-card-template/SKILL.md):自定义卡片模板的创作(上下文契约、实战配方、设计模式库、坑与验收);本技能第十一章是其机制前提
|
||
- [ulthon-scheme-curd-workflow](./ulthon-scheme-curd-workflow/SKILL.md):CURD 代码生成流程(生成本技能所定制的列表页骨架)
|
||
- [ulthon-scheme-definition](./ulthon-scheme-definition/SKILL.md):Scheme 定义(决定生成哪些字段、字段类型)
|
||
- [ulthon-page-api-dual-mode](./ulthon-page-api-dual-mode/SKILL.md):页面/接口同体机制(列表接口的 JSON 返回模式)
|
||
- [ulthon-permission-cli](./ulthon-permission-cli/SKILL.md):权限节点核对(按钮 auth 值与 `@NodeAnnotation` 注册节点对不上时)
|
||
- [ulthon-tools-http-call](./ulthon-tools-http-call/SKILL.md):CLI 模拟请求验证列表接口
|
||
- [ulthon-page-qa](./ulthon-page-qa/SKILL.md):页面验证(AI 自测)与浏览器断言要点
|